diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..6eb7a3ea
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,55 @@
+# 🚀 Claude Relay Service Configuration
+
+# 🌐 服务器配置
+PORT=3000
+HOST=0.0.0.0
+NODE_ENV=production
+
+# 🔐 安全配置
+JWT_SECRET=your-jwt-secret-here
+ADMIN_SESSION_TIMEOUT=86400000
+API_KEY_PREFIX=cr_
+ENCRYPTION_KEY=your-encryption-key-here
+
+# 📊 Redis 配置
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=0
+
+# 🎯 Claude API 配置
+CLAUDE_API_URL=https://api.anthropic.com/v1/messages
+CLAUDE_API_VERSION=2023-06-01
+CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14
+
+# 🌐 代理配置
+DEFAULT_PROXY_TIMEOUT=30000
+MAX_PROXY_RETRIES=3
+
+# 📈 使用限制
+DEFAULT_TOKEN_LIMIT=1000000
+DEFAULT_REQUEST_LIMIT=1000
+
+# 🚦 速率限制
+RATE_LIMIT_WINDOW=60000
+RATE_LIMIT_MAX_REQUESTS=100
+
+# 📝 日志配置
+LOG_LEVEL=info
+LOG_MAX_SIZE=10m
+LOG_MAX_FILES=5
+
+# 🔧 系统配置
+CLEANUP_INTERVAL=3600000
+TOKEN_USAGE_RETENTION=2592000000
+HEALTH_CHECK_INTERVAL=60000
+
+# 🎨 Web 界面配置
+WEB_TITLE=Claude Relay Service
+WEB_DESCRIPTION=Multi-account Claude API relay service with beautiful management interface
+WEB_LOGO_URL=/assets/logo.png
+
+# 🛠️ 开发配置
+DEBUG=false
+ENABLE_CORS=true
+TRUST_PROXY=true
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 00000000..a8fbd64a
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,19 @@
+module.exports = {
+ env: {
+ node: true,
+ es2022: true,
+ },
+ extends: [
+ 'eslint:recommended',
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ rules: {
+ 'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
+ 'no-console': 'off',
+ 'quotes': ['error', 'single'],
+ 'semi': ['error', 'always'],
+ },
+};
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..4cf0fb8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,229 @@
+# Dependencies
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Environment variables
+.env
+.env.*
+!.env.example
+
+# Claude specific directories
+.claude/
+
+# Data directory (contains sensitive information)
+data/
+!data/.gitkeep
+
+# Logs directory
+logs/
+*.log
+startup.log
+app.log
+
+# Configuration files (may contain sensitive data)
+config/config.js
+!config/config.example.js
+
+# Runtime data
+pids/
+*.pid
+*.seed
+*.pid.lock
+
+# Coverage directory used by tools like istanbul
+coverage/
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage
+.grunt
+
+# Bower dependency directory
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons
+build/Release
+
+# Dependency directories
+jspm_packages/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# parcel-bundler cache
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+public
+
+# Vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# Temporary folders
+tmp/
+temp/
+.tmp/
+.temp/
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+desktop.ini
+
+# IDE files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Backup files
+*.bak
+*.backup
+*~
+
+# Archive files (unless specifically needed)
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Application specific files
+# JWT secrets and encryption keys
+secrets/
+keys/
+certs/
+
+# Database dumps
+*.sql
+*.db
+*.sqlite
+*.sqlite3
+
+# Redis dumps
+dump.rdb
+appendonly.aof
+
+# PM2 files
+ecosystem.config.js
+.pm2/
+
+# Docker files (keep main ones, ignore volumes)
+.docker/
+docker-volumes/
+
+# Monitoring data
+prometheus/
+grafana/
+
+# Test files and coverage
+test-results/
+coverage/
+.nyc_output/
+
+# Documentation build
+docs/build/
+docs/dist/
+
+# Deployment files
+deploy/
+.deploy/
+
+# Package lock files (choose one)
+# Uncomment the one you DON'T want to track
+# package-lock.json
+# yarn.lock
+# pnpm-lock.yaml
+
+# Local development files
+.local/
+local/
+
+# Debug files
+debug.log
+error.log
+access.log
+
+# Session files
+sessions/
+
+# Upload directories
+uploads/
+files/
+
+# Cache directories
+.cache/
+cache/
+
+# Build artifacts
+build/
+dist/
+out/
+
+# Runtime files
+*.sock
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..a3ee2d0c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,181 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+这个文件为 Claude Code (claude.ai/code) 提供在此代码库中工作的指导。
+
+## 项目概述
+
+Claude Relay Service 是一个功能完整的 Claude API 中转服务,支持多账户管理、API Key 认证、代理配置和现代化 Web 管理界面。该服务作为客户端(如 SillyTavern)与 Anthropic API 之间的中间件,提供认证、限流、监控等功能。
+
+## 核心架构
+
+### 关键架构概念
+- **代理认证流**: 客户端用自建API Key → 验证 → 获取Claude账户OAuth token → 转发到Anthropic
+- **Token管理**: 自动监控OAuth token过期并刷新,支持10秒提前刷新策略
+- **代理支持**: 每个Claude账户支持独立代理配置,OAuth token交换也通过代理进行
+- **数据加密**: 敏感数据(refreshToken, accessToken)使用AES加密存储在Redis
+
+### 主要服务组件
+- **claudeRelayService.js**: 核心代理服务,处理请求转发和流式响应
+- **claudeAccountService.js**: Claude账户管理,OAuth token刷新和账户选择
+- **apiKeyService.js**: API Key管理,验证、限流和使用统计
+- **oauthHelper.js**: OAuth工具,PKCE流程实现和代理支持
+
+### 认证和代理流程
+1. 客户端使用自建API Key(cr_前缀格式)发送请求
+2. authenticateApiKey中间件验证API Key有效性和速率限制
+3. claudeAccountService自动选择可用Claude账户
+4. 检查OAuth access token有效性,过期则自动刷新(使用代理)
+5. 移除客户端API Key,使用OAuth Bearer token转发请求
+6. 通过账户配置的代理发送到Anthropic API
+7. 流式或非流式返回响应,记录使用统计
+
+### OAuth集成
+- **PKCE流程**: 完整的OAuth 2.0 PKCE实现,支持代理
+- **自动刷新**: 智能token过期检测和自动刷新机制
+- **代理支持**: OAuth授权和token交换全程支持代理配置
+- **安全存储**: claudeAiOauth数据加密存储,包含accessToken、refreshToken、scopes
+
+## 常用命令
+
+### 基本开发命令
+```bash
+# 安装依赖和初始化
+npm install
+npm run setup # 生成配置和管理员凭据
+npm run install:web # 安装Web界面依赖
+
+# 开发和运行
+npm run dev # 开发模式(热重载)
+npm start # 生产模式
+npm test # 运行测试
+npm run lint # 代码检查
+
+# Docker部署
+docker-compose up -d # 推荐方式
+docker-compose --profile monitoring up -d # 包含监控
+
+# 服务管理
+npm run service:start:daemon # 后台启动(推荐)
+npm run service:status # 查看服务状态
+npm run service:logs # 查看日志
+npm run service:stop # 停止服务
+
+# CLI管理工具
+npm run cli admin # 管理员操作
+npm run cli keys # API Key管理
+npm run cli accounts # Claude账户管理
+npm run cli status # 系统状态
+```
+
+### 开发环境配置
+必须配置的环境变量:
+- `JWT_SECRET`: JWT密钥(32字符以上随机字符串)
+- `ENCRYPTION_KEY`: 数据加密密钥(32字符固定长度)
+- `REDIS_HOST`: Redis主机地址(默认localhost)
+- `REDIS_PORT`: Redis端口(默认6379)
+- `REDIS_PASSWORD`: Redis密码(可选)
+
+初始化命令:
+```bash
+cp config/config.example.js config/config.js
+cp .env.example .env
+npm run setup # 自动生成密钥并创建管理员账户
+```
+
+## Web界面功能
+
+### OAuth账户添加流程
+1. **基本信息和代理设置**: 配置账户名称、描述和代理参数
+2. **OAuth授权**:
+ - 生成授权URL → 用户打开链接并登录Claude Code账号
+ - 授权后会显示Authorization Code → 复制并粘贴到输入框
+ - 系统自动交换token并创建账户
+
+### 核心管理功能
+- **实时仪表板**: 系统统计、账户状态、使用量监控
+- **API Key管理**: 创建、配额设置、使用统计查看
+- **Claude账户管理**: OAuth账户添加、代理配置、状态监控
+- **系统日志**: 实时日志查看,多级别过滤
+
+## 重要端点
+
+### API转发端点
+- `POST /api/v1/messages` - 主要消息处理端点(支持流式)
+- `GET /api/v1/models` - 模型列表(兼容性)
+- `GET /api/v1/usage` - 使用统计查询
+- `GET /api/v1/key-info` - API Key信息
+
+### OAuth管理端点
+- `POST /admin/claude-accounts/generate-auth-url` - 生成OAuth授权URL(含代理)
+- `POST /admin/claude-accounts/exchange-code` - 交换authorization code
+- `POST /admin/claude-accounts` - 创建OAuth账户
+
+### 系统端点
+- `GET /health` - 健康检查
+- `GET /web` - Web管理界面
+- `GET /admin/dashboard` - 系统概览数据
+
+## 故障排除
+
+### OAuth相关问题
+1. **代理配置错误**: 检查代理设置是否正确,OAuth token交换也需要代理
+2. **授权码无效**: 确保复制了完整的Authorization Code,没有遗漏字符
+3. **Token刷新失败**: 检查refreshToken有效性和代理配置
+
+### 常见开发问题
+1. **Redis连接失败**: 确认Redis服务运行,检查连接配置
+2. **管理员登录失败**: 检查init.json同步到Redis,运行npm run setup
+3. **API Key格式错误**: 确保使用cr_前缀格式
+4. **代理连接问题**: 验证SOCKS5/HTTP代理配置和认证信息
+
+### 调试工具
+- **日志系统**: Winston结构化日志,支持不同级别
+- **CLI工具**: 命令行状态查看和管理
+- **Web界面**: 实时日志查看和系统监控
+- **健康检查**: /health端点提供系统状态
+
+## 开发最佳实践
+
+### 代码修改原则
+- 对现有文件进行修改时,首先检查代码库的现有模式和风格
+- 尽可能重用现有的服务和工具函数,避免重复代码
+- 遵循项目现有的错误处理和日志记录模式
+- 敏感数据必须使用加密存储(参考 claudeAccountService.js 中的加密实现)
+
+### 测试和质量保证
+- 运行 `npm run lint` 进行代码风格检查(使用 ESLint)
+- 运行 `npm test` 执行测试套件(Jest + SuperTest 配置)
+- 在修改核心服务后,使用 CLI 工具验证功能:`npm run cli status`
+- 检查日志文件 `logs/claude-relay-*.log` 确认服务正常运行
+- 注意:当前项目缺少实际测试文件,建议补充单元测试和集成测试
+
+### 常见文件位置
+- 核心服务逻辑:`src/services/` 目录
+- 路由处理:`src/routes/` 目录
+- 中间件:`src/middleware/` 目录
+- 配置管理:`config/config.js`
+- Redis 模型:`src/models/redis.js`
+- 工具函数:`src/utils/` 目录
+
+### 重要架构决策
+- 所有敏感数据(OAuth token、refreshToken)都使用 AES 加密存储在 Redis
+- 每个 Claude 账户支持独立的代理配置,包括 SOCKS5 和 HTTP 代理
+- API Key 使用哈希存储,支持 `cr_` 前缀格式
+- 请求流程:API Key 验证 → 账户选择 → Token 刷新(如需)→ 请求转发
+- 支持流式和非流式响应,客户端断开时自动清理资源
+
+### 核心数据流和性能优化
+- **哈希映射优化**: API Key 验证从 O(n) 优化到 O(1) 查找
+- **智能 Usage 捕获**: 从 SSE 流中解析真实的 token 使用数据
+- **多维度统计**: 支持按时间、模型、用户的实时使用统计
+- **异步处理**: 非阻塞的统计记录和日志写入
+- **原子操作**: Redis 管道操作确保数据一致性
+
+### 安全和容错机制
+- **多层加密**: API Key 哈希 + OAuth Token AES 加密
+- **零信任验证**: 每个请求都需要完整的认证链
+- **优雅降级**: Redis 连接失败时的回退机制
+- **自动重试**: 指数退避重试策略和错误隔离
+- **资源清理**: 客户端断开时的自动清理机制
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..f8e96857
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,48 @@
+# 🐳 使用官方 Node.js 18 Alpine 镜像
+FROM node:18-alpine
+
+# 📋 设置标签
+LABEL maintainer="claude-relay-service@example.com"
+LABEL description="Claude Code API Relay Service"
+LABEL version="1.0.0"
+
+# 🔧 安装系统依赖
+RUN apk add --no-cache \
+ curl \
+ dumb-init \
+ && rm -rf /var/cache/apk/*
+
+# 👤 创建应用用户
+RUN addgroup -g 1001 -S nodejs && \
+ adduser -S claude -u 1001 -G nodejs
+
+# 📁 设置工作目录
+WORKDIR /app
+
+# 📦 复制 package 文件
+COPY package*.json ./
+
+# 🔽 安装依赖 (生产环境)
+RUN npm ci --only=production && \
+ npm cache clean --force
+
+# 📋 复制应用代码
+COPY --chown=claude:nodejs . .
+
+# 📁 创建必要目录
+RUN mkdir -p logs data temp && \
+ chown -R claude:nodejs logs data temp
+
+# 🔐 切换到非 root 用户
+USER claude
+
+# 🌐 暴露端口
+EXPOSE 3000
+
+# 🏥 健康检查
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+ CMD curl -f http://localhost:3000/health || exit 1
+
+# 🚀 启动应用
+ENTRYPOINT ["dumb-init", "--"]
+CMD ["node", "src/app.js"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 7a94ca9d..00000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2025 Wesley Liddick
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..d4b22b54
--- /dev/null
+++ b/README.md
@@ -0,0 +1,407 @@
+# Claude Relay Service
+
+
+
+[](https://opensource.org/licenses/MIT)
+[](https://nodejs.org/)
+[](https://redis.io/)
+[](https://www.docker.com/)
+
+**🔐 自己搭建的Claude API中转服务,支持多账户管理**
+
+[English](#english) • [中文文档](#中文文档)
+
+
+
+---
+
+## ⚠️ 重要提醒
+
+**使用本项目前请仔细阅读:**
+
+🚨 **服务条款风险**: 使用本项目可能违反Anthropic的服务条款。请在使用前仔细阅读Anthropic的用户协议,使用本项目的一切风险由用户自行承担。
+
+📖 **免责声明**: 本项目仅供技术学习和研究使用,作者不对因使用本项目导致的账户封禁、服务中断或其他损失承担任何责任。
+
+---
+
+## 🤔 这个项目适合你吗?
+
+- 🌍 **地区限制**: 所在地区无法直接访问Claude Code服务?
+- 🔒 **隐私担忧**: 担心第三方镜像服务会记录或泄露你的对话内容?
+- 👥 **成本分摊**: 想和朋友一起分摊Claude Code Max订阅费用?
+- ⚡ **稳定性**: 第三方镜像站经常故障不稳定,影响效率 ?
+
+如果有以上困惑,那这个项目可能适合你。
+
+### 适合的场景
+
+✅ **找朋友拼车**: 三五好友一起分摊Claude Code Max订阅,Opus爽用
+✅ **隐私敏感**: 不想让第三方镜像看到你的对话内容
+✅ **技术折腾**: 有基本的技术基础,愿意自己搭建和维护
+✅ **稳定需求**: 需要长期稳定的Claude访问,不想受制于镜像站
+✅ **地区受限**: 无法直接访问Claude官方服务
+
+### 不适合的场景
+
+❌ **纯小白**: 完全不懂技术,连服务器都不会买
+❌ **偶尔使用**: 一个月用不了几次,没必要折腾
+❌ **注册问题**: 无法自行注册Claude账号
+❌ **支付问题**: 没有支付渠道订阅Claude Code
+
+---
+
+## 💭 为什么要自己搭?
+
+
+### 现有镜像站可能的问题
+
+- 🕵️ **隐私风险**: 你的对话内容都被人家看得一清二楚,商业机密什么的就别想了
+- 🐌 **性能不稳**: 用的人多了就慢,高峰期经常卡死
+- 💰 **价格不透明**: 不知道实际成本
+
+### 自建的好处
+
+- 🔐 **数据安全**: 所有接口请求都只经过你自己的服务器,直连Anthropic API
+- ⚡ **性能可控**: 就你们几个人用,Max 200刀套餐基本上可以爽用Opus
+- 💰 **成本透明**: 用了多少token一目了然,按官方价格换算了具体费用
+- 📊 **监控完整**: 使用情况、成本分析、性能监控全都有
+
+---
+
+## 🚀 核心功能
+
+### 基础功能
+- ✅ **多账户管理**: 可以添加多个Claude账户自动轮换
+- ✅ **自定义API Key**: 给每个人分配独立的Key
+- ✅ **使用统计**: 详细记录每个人用了多少token
+
+### 高级功能
+- 🔄 **智能切换**: 账户出问题自动换下一个
+- 🚀 **性能优化**: 连接池、缓存,减少延迟
+- 📊 **监控面板**: Web界面查看所有数据
+- 🛡️ **安全控制**: 访问限制、速率控制
+- 🌐 **代理支持**: 支持HTTP/SOCKS5代理
+
+---
+
+## 📋 部署要求
+
+### 硬件要求(最低配置)
+- **CPU**: 1核心就够了
+- **内存**: 512MB(建议1GB)
+- **硬盘**: 30GB可用空间
+- **网络**: 能访问到Anthropic API(建议使用US地区的机器)
+- **建议**: 2核4G的基本够了,网络尽量选回国线路快一点的(为了提高速度,建议不要开代理或者设置服务器的IP直连)
+
+### 软件要求
+- **Node.js** 18或更高版本
+- **Redis** 6或更高版本
+- **操作系统**: 建议Linux
+
+### 费用估算
+- **服务器**: 轻量云服务器,一个月10-30块
+- **Claude订阅**: 看你怎么分摊了
+- **其他**: 基本没有了
+
+---
+
+## 🐳 最简单的部署方式(Docker)
+
+如果你懒得折腾环境,直接用Docker:
+
+```bash
+# 1. 下载项目
+git clone https://github.com/yourusername/claude-relay-service.git
+cd claude-relay-service
+
+# 2. 一键启动
+docker-compose up -d
+
+# 3. 查看是否启动成功
+docker-compose ps
+```
+
+就这么简单,服务就跑起来了。
+
+---
+
+## 📦 手动部署(适合折腾党)
+
+### 第一步:环境准备
+
+**Ubuntu/Debian用户:**
+```bash
+# 安装Node.js
+curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
+sudo apt-get install -y nodejs
+
+# 安装Redis
+sudo apt update
+sudo apt install redis-server
+sudo systemctl start redis-server
+```
+
+**CentOS/RHEL用户:**
+```bash
+# 安装Node.js
+curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
+sudo yum install -y nodejs
+
+# 安装Redis
+sudo yum install redis
+sudo systemctl start redis
+```
+
+### 第二步:下载和配置
+
+```bash
+# 下载项目
+git clone https://github.com/yourusername/claude-relay-service.git
+cd claude-relay-service
+
+# 安装依赖
+npm install
+
+# 复制配置文件(重要!)
+cp config/config.example.js config/config.js
+cp .env.example .env
+```
+
+### 第三步:配置文件设置
+
+**编辑 `.env` 文件:**
+```bash
+# 这两个密钥随便生成,但要记住
+JWT_SECRET=你的超级秘密密钥
+ENCRYPTION_KEY=32位的加密密钥随便写
+
+# Redis配置
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_PASSWORD=
+```
+
+**编辑 `config/config.js` 文件:**
+```javascript
+module.exports = {
+ server: {
+ port: 3000, // 服务端口,可以改
+ host: '0.0.0.0' // 不用改
+ },
+ redis: {
+ host: '127.0.0.1', // Redis地址
+ port: 6379 // Redis端口
+ },
+ // 其他配置保持默认就行
+}
+```
+
+### 第四步:启动服务
+
+```bash
+# 初始化
+npm run setup
+
+# 启动服务
+npm run service:start:daemon # 后台运行(推荐)
+
+# 查看状态
+npm run service:status
+```
+
+---
+
+## 🎮 开始使用
+
+### 1. 打开管理界面
+
+浏览器访问:`http://你的服务器IP:3000/web`
+
+默认管理员账号:admin / admin123
+
+### 2. 添加Claude账户
+
+这一步比较关键,需要OAuth授权:
+
+1. 点击「Claude账户」标签
+2. 如果你在国内,先配置代理(重要!)
+3. 点击「添加账户」
+4. 点击「生成授权链接」,会打开一个新页面
+5. 在新页面完成Claude登录和授权
+6. 复制返回的Authorization Code
+7. 粘贴到页面完成添加
+
+**注意**: 如果你在国内,这一步可能需要科学上网。
+
+### 3. 创建API Key
+
+给每个使用者分配一个Key:
+
+1. 点击「API Keys」标签
+2. 点击「创建新Key」
+3. 给Key起个名字,比如「张三的Key」
+4. 设置使用限制(可选)
+5. 保存,记下生成的Key
+
+### 4. 开始使用API
+
+现在你可以用自己的服务替换官方API了:
+
+**原来的请求:**
+```bash
+curl https://api.anthropic.com/v1/messages \
+ -H "x-api-key: 官方的key" \
+ -H "content-type: application/json" \
+ -d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"你好"}]}'
+```
+
+**现在的请求:**
+```bash
+curl http://你的域名:3000/api/v1/messages \
+ -H "x-api-key: cr_你创建的key" \
+ -H "content-type: application/json" \
+ -d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"你好"}]}'
+```
+
+就是把域名换一下,API Key换成你自己生成的,其他都一样。
+
+---
+
+## 🔧 日常维护
+
+### 服务管理
+
+```bash
+# 查看服务状态
+npm run service:status
+
+# 查看日志
+npm run service:logs
+
+# 重启服务
+npm run service:restart:daemon
+
+# 停止服务
+npm run service:stop
+```
+
+### 监控使用情况
+
+- **Web界面**: `http://你的域名:3000/web` - 查看使用统计
+- **健康检查**: `http://你的域名:3000/health` - 确认服务正常
+- **日志文件**: `logs/` 目录下的各种日志文件
+
+### 常见问题处理
+
+**Redis连不上?**
+```bash
+# 检查Redis是否启动
+redis-cli ping
+
+# 应该返回 PONG
+```
+
+**OAuth授权失败?**
+- 检查代理设置是否正确
+- 确保能正常访问 claude.ai
+- 清除浏览器缓存重试
+
+**API请求失败?**
+- 检查API Key是否正确
+- 查看日志文件找错误信息
+- 确认Claude账户状态正常
+
+---
+
+## 🛠️ 高级玩法
+
+### 设置代理(国内用户必看)
+
+如果你在国内,需要配置代理才能正常使用:
+
+```javascript
+// 在账户配置中添加
+{
+ "proxy": {
+ "type": "socks5", // 或者 "http"
+ "host": "127.0.0.1",
+ "port": 1080,
+ "username": "用户名", // 如果代理需要认证
+ "password": "密码" // 如果代理需要认证
+ }
+}
+```
+
+### 命令行管理工具
+
+懒得打开网页?用命令行:
+
+```bash
+# 查看所有API Key
+npm run cli keys list
+
+# 创建新Key
+npm run cli keys create --name "测试Key" --limit 1000
+
+# 查看账户状态
+npm run cli accounts list
+
+# 测试账户连接
+npm run cli accounts test --id 账户ID
+```
+
+### 监控集成
+
+如果你想要更专业的监控,可以接入Prometheus:
+
+访问 `http://你的域名(或IP):3000/metrics` 获取指标数据。
+
+---
+
+## 💡 使用建议
+
+### 账户管理
+- **多账户**: 建议添加2-3个Claude账户,防止单点故障
+- **定期检查**: 每周看看账户状态,及时处理异常
+- **备用方案**: 准备几个备用账户,关键时刻能顶上
+
+### 成本控制
+- **设置限额**: 给每个API Key设置合理的使用限制
+- **监控支出**: 定期查看成本统计,控制预算
+- **合理分配**: 根据使用频率分配配额
+
+### 安全建议
+- **定期备份**: 重要配置和数据要备份
+- **监控日志**: 定期查看异常日志
+- **更新密钥**: 定期更换JWT和加密密钥
+
+---
+
+## 🆘 遇到问题怎么办?
+
+### 自助排查
+1. **查看日志**: `logs/` 目录下的日志文件
+2. **检查配置**: 确认配置文件设置正确
+3. **测试连通性**: 用 curl 测试API是否正常
+4. **重启服务**: 有时候重启一下就好了
+
+### 寻求帮助
+- **GitHub Issues**: 提交详细的错误信息
+- **查看文档**: 仔细阅读错误信息和文档
+- **社区讨论**: 看看其他人是否遇到类似问题
+
+---
+
+## 📄 许可证
+本项目采用 [MIT许可证](LICENSE)。
+
+---
+
+
+
+**⭐ 觉得有用的话给个Star呗,这是对作者最大的鼓励!**
+
+**🤝 有问题欢迎提Issue,有改进建议欢迎PR**
+
+
\ No newline at end of file
diff --git a/README_EN.md b/README_EN.md
new file mode 100644
index 00000000..e666b37e
--- /dev/null
+++ b/README_EN.md
@@ -0,0 +1,408 @@
+# Claude Relay Service
+
+
+
+[](https://opensource.org/licenses/MIT)
+[](https://nodejs.org/)
+[](https://redis.io/)
+[](https://www.docker.com/)
+
+**🔐 Self-hosted Claude API relay service with multi-account management**
+
+[English](#english) • [中文文档](#中文文档)
+
+
+
+---
+
+## ⚠️ Important Notice
+
+**Please read carefully before using this project:**
+
+🚨 **Terms of Service Risk**: Using this project may violate Anthropic's terms of service. Please carefully read Anthropic's user agreement before use. All risks from using this project are borne by the user.
+
+📖 **Disclaimer**: This project is for technical learning and research purposes only. The author is not responsible for any account bans, service interruptions, or other losses caused by using this project.
+
+---
+
+## 🤔 Is This Project Right for You?
+
+- 🌍 **Regional Restrictions**: Can't directly access Claude Code service in your region?
+- 🔒 **Privacy Concerns**: Worried about third-party mirror services logging or leaking your conversation content?
+- 👥 **Cost Sharing**: Want to share Claude Code Max subscription costs with friends?
+- ⚡ **Stability**: Third-party mirror sites often have outages and instability, affecting efficiency?
+
+If you nodded yes, this project might be for you.
+
+### Suitable Scenarios
+
+✅ **Cost Sharing with Friends**: 3-5 friends sharing Claude Code Max subscription, enjoying Opus freely
+✅ **Privacy Sensitive**: Don't want third parties to see your conversation content
+✅ **Technical Tinkering**: Have basic technical skills, willing to build and maintain yourself
+✅ **Stability Needs**: Need long-term stable Claude access, don't want to be restricted by mirror sites
+✅ **Regional Restrictions**: Cannot directly access Claude official service
+
+### Unsuitable Scenarios
+
+❌ **Complete Beginner**: Don't understand technology at all, don't even know how to buy a server
+❌ **Occasional Use**: Use it only a few times a month, not worth the hassle
+❌ **Registration Issues**: Cannot register Claude account yourself
+❌ **Payment Issues**: No payment method to subscribe to Claude Code
+
+---
+
+## 💭 Why Build Your Own?
+
+Honestly, there are quite a few Claude proxy services online now, but there are also many issues:
+
+### Problems with Existing Proxies
+
+- 🕵️ **Privacy Risk**: Your conversation content is seen clearly by others, forget about business secrets
+- 🐌 **Performance Instability**: Slow when many people use it, often crashes during peak hours
+- 💰 **Price Opacity**: Don't know the actual costs
+
+### Benefits of Self-hosting
+
+- 🔐 **Data Security**: All API requests only go through your own server, direct connection to Anthropic API
+- ⚡ **Controllable Performance**: Only a few of you using it, as fast as you want
+- 💰 **Cost Transparency**: Clear view of how many tokens used, specific costs calculated at official prices
+- 📊 **Complete Monitoring**: Usage statistics, cost analysis, performance monitoring all available
+
+---
+
+## 🚀 Core Features
+
+### Basic Features
+- ✅ **Multi-account Management**: Add multiple Claude accounts for automatic rotation
+- ✅ **Custom API Keys**: Assign independent keys to each person
+- ✅ **Usage Statistics**: Detailed records of how many tokens each person used
+
+### Advanced Features
+- 🔄 **Smart Switching**: Automatically switch to next account when one has issues
+- 🚀 **Performance Optimization**: Connection pooling, caching to reduce latency
+- 📊 **Monitoring Dashboard**: Web interface to view all data
+- 🛡️ **Security Control**: Access restrictions, rate limiting
+- 🌐 **Proxy Support**: Support for HTTP/SOCKS5 proxies
+
+---
+
+## 📋 Deployment Requirements
+
+### Hardware Requirements (Minimum Configuration)
+- **CPU**: 1 core is enough
+- **Memory**: 512MB (1GB recommended)
+- **Storage**: 30GB available space
+- **Network**: Access to Anthropic API (recommend US region servers)
+- **Suggestion**: 2 cores 4GB is basically enough, choose network with good return routes to your country (to improve speed, recommend not using proxy or setting server IP for direct connection)
+
+### Software Requirements
+- **Node.js** 18 or higher
+- **Redis** 6 or higher
+- **Operating System**: Linux recommended
+
+### Cost Estimation
+- **Server**: Light cloud server, 10-30 RMB per month
+- **Claude Subscription**: Depends on how you share costs
+- **Others**: Basically none
+
+---
+
+## 🐳 Simplest Deployment Method (Docker)
+
+If you're too lazy to set up the environment, use Docker directly:
+
+```bash
+# 1. Download project
+git clone https://github.com/yourusername/claude-relay-service.git
+cd claude-relay-service
+
+# 2. One-click start
+docker-compose up -d
+
+# 3. Check if started successfully
+docker-compose ps
+```
+
+That simple, the service is running.
+
+---
+
+## 📦 Manual Deployment (For Tinkerers)
+
+### Step 1: Environment Setup
+
+**Ubuntu/Debian users:**
+```bash
+# Install Node.js
+curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
+sudo apt-get install -y nodejs
+
+# Install Redis
+sudo apt update
+sudo apt install redis-server
+sudo systemctl start redis-server
+```
+
+**CentOS/RHEL users:**
+```bash
+# Install Node.js
+curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
+sudo yum install -y nodejs
+
+# Install Redis
+sudo yum install redis
+sudo systemctl start redis
+```
+
+### Step 2: Download and Configure
+
+```bash
+# Download project
+git clone https://github.com/yourusername/claude-relay-service.git
+cd claude-relay-service
+
+# Install dependencies
+npm install
+
+# Copy configuration files (Important!)
+cp config/config.example.js config/config.js
+cp .env.example .env
+```
+
+### Step 3: Configuration File Setup
+
+**Edit `.env` file:**
+```bash
+# Generate these two keys randomly, but remember them
+JWT_SECRET=your-super-secret-key
+ENCRYPTION_KEY=32-character-encryption-key-write-randomly
+
+# Redis configuration
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_PASSWORD=
+```
+
+**Edit `config/config.js` file:**
+```javascript
+module.exports = {
+ server: {
+ port: 3000, // Service port, can be changed
+ host: '0.0.0.0' // Don't change
+ },
+ redis: {
+ host: '127.0.0.1', // Redis address
+ port: 6379 // Redis port
+ },
+ // Keep other configurations as default
+}
+```
+
+### Step 4: Start Service
+
+```bash
+# Initialize
+npm run setup
+
+# Start service
+npm run service:start:daemon # Run in background (recommended)
+
+# Check status
+npm run service:status
+```
+
+---
+
+## 🎮 Getting Started
+
+### 1. Open Management Interface
+
+Browser visit: `http://your-server-IP:3000/web`
+
+Default admin account: admin / admin123
+
+### 2. Add Claude Account
+
+This step is quite important, requires OAuth authorization:
+
+1. Click "Claude Accounts" tab
+2. If you're in China, configure proxy first (Important!)
+3. Click "Add Account"
+4. Click "Generate Authorization Link", will open a new page
+5. Complete Claude login and authorization in the new page
+6. Copy the returned Authorization Code
+7. Paste to page to complete addition
+
+**Note**: If you're in China, this step may require VPN.
+
+### 3. Create API Key
+
+Assign a key to each user:
+
+1. Click "API Keys" tab
+2. Click "Create New Key"
+3. Give the key a name, like "Zhang San's Key"
+4. Set usage limits (optional)
+5. Save, note down the generated key
+
+### 4. Start Using API
+
+Now you can replace the official API with your own service:
+
+**Original request:**
+```bash
+curl https://api.anthropic.com/v1/messages \
+ -H "x-api-key: official-key" \
+ -H "content-type: application/json" \
+ -d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"Hello"}]}'
+```
+
+**Current request:**
+```bash
+curl http://your-domain:3000/api/v1/messages \
+ -H "x-api-key: cr_your-created-key" \
+ -H "content-type: application/json" \
+ -d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"Hello"}]}'
+```
+
+Just change the domain and API Key to your own generated one, everything else is the same.
+
+---
+
+## 🔧 Daily Maintenance
+
+### Service Management
+
+```bash
+# Check service status
+npm run service:status
+
+# View logs
+npm run service:logs
+
+# Restart service
+npm run service:restart:daemon
+
+# Stop service
+npm run service:stop
+```
+
+### Monitor Usage
+
+- **Web Interface**: `http://your-domain:3000/web` - View usage statistics
+- **Health Check**: `http://your-domain:3000/health` - Confirm service is normal
+- **Log Files**: Various log files in `logs/` directory
+
+### Common Issue Resolution
+
+**Can't connect to Redis?**
+```bash
+# Check if Redis is running
+redis-cli ping
+
+# Should return PONG
+```
+
+**OAuth authorization failed?**
+- Check if proxy settings are correct
+- Ensure normal access to claude.ai
+- Clear browser cache and retry
+
+**API request failed?**
+- Check if API Key is correct
+- View log files for error information
+- Confirm Claude account status is normal
+
+---
+
+## 🛠️ Advanced Usage
+
+### Setting Up Proxy (Must-read for Chinese Users)
+
+If you're in China, you need to configure proxy to use normally:
+
+```javascript
+// Add in account configuration
+{
+ "proxy": {
+ "type": "socks5", // or "http"
+ "host": "127.0.0.1",
+ "port": 1080,
+ "username": "username", // if proxy requires authentication
+ "password": "password" // if proxy requires authentication
+ }
+}
+```
+
+### Command Line Management Tool
+
+Too lazy to open webpage? Use command line:
+
+```bash
+# View all API Keys
+npm run cli keys list
+
+# Create new Key
+npm run cli keys create --name "Test Key" --limit 1000
+
+# View account status
+npm run cli accounts list
+
+# Test account connection
+npm run cli accounts test --id account-ID
+```
+
+### Monitoring Integration
+
+If you want more professional monitoring, you can integrate Prometheus:
+
+Visit `http://your-domain(or-IP):3000/metrics` to get metrics data.
+
+---
+
+## 💡 Usage Recommendations
+
+### Account Management
+- **Multiple Accounts**: Recommend adding 2-3 Claude accounts to prevent single point of failure
+- **Regular Checks**: Check account status weekly, handle exceptions promptly
+- **Backup Plan**: Prepare several backup accounts that can step in during critical moments
+
+### Cost Control
+- **Set Limits**: Set reasonable usage limits for each API Key
+- **Monitor Spending**: Regularly check cost statistics, control budget
+- **Reasonable Allocation**: Allocate quotas based on usage frequency
+
+### Security Recommendations
+- **Regular Backups**: Back up important configurations and data
+- **Monitor Logs**: Regularly check exception logs
+- **Update Keys**: Regularly change JWT and encryption keys
+
+---
+
+## 🆘 What to Do When You Encounter Problems?
+
+### Self-troubleshooting
+1. **Check Logs**: Log files in `logs/` directory
+2. **Check Configuration**: Confirm configuration files are set correctly
+3. **Test Connectivity**: Use curl to test if API is normal
+4. **Restart Service**: Sometimes restarting fixes it
+
+### Seeking Help
+- **GitHub Issues**: Submit detailed error information
+- **Read Documentation**: Carefully read error messages and documentation
+- **Community Discussion**: See if others have encountered similar problems
+
+---
+
+## 📄 License
+This project uses the [MIT License](LICENSE).
+
+---
+
+
+
+**⭐ If you find it useful, please give it a Star, this is the greatest encouragement to the author!**
+
+**🤝 Feel free to submit Issues for problems, welcome PRs for improvement suggestions**
+
+
\ No newline at end of file
diff --git a/cli/index.js b/cli/index.js
new file mode 100644
index 00000000..a27002a1
--- /dev/null
+++ b/cli/index.js
@@ -0,0 +1,772 @@
+#!/usr/bin/env node
+
+const { Command } = require('commander');
+const inquirer = require('inquirer');
+const chalk = require('chalk');
+const ora = require('ora');
+const Table = require('table').table;
+const bcrypt = require('bcryptjs');
+const crypto = require('crypto');
+
+const config = require('../config/config');
+const redis = require('../src/models/redis');
+const apiKeyService = require('../src/services/apiKeyService');
+const claudeAccountService = require('../src/services/claudeAccountService');
+
+const program = new Command();
+
+// 🎨 样式
+const styles = {
+ title: chalk.bold.blue,
+ success: chalk.green,
+ error: chalk.red,
+ warning: chalk.yellow,
+ info: chalk.cyan,
+ dim: chalk.dim
+};
+
+// 🔧 初始化
+async function initialize() {
+ const spinner = ora('正在连接 Redis...').start();
+ try {
+ await redis.connect();
+ spinner.succeed('Redis 连接成功');
+ } catch (error) {
+ spinner.fail('Redis 连接失败');
+ console.error(styles.error(error.message));
+ process.exit(1);
+ }
+}
+
+// 🔐 管理员账户管理
+program
+ .command('admin')
+ .description('管理员账户操作')
+ .action(async () => {
+ await initialize();
+
+ const { action } = await inquirer.prompt({
+ type: 'list',
+ name: 'action',
+ message: '选择操作:',
+ choices: [
+ { name: '🔑 设置管理员密码', value: 'set-password' },
+ { name: '👤 创建初始管理员', value: 'create-admin' },
+ { name: '🔄 重置管理员密码', value: 'reset-password' },
+ { name: '📊 查看管理员信息', value: 'view-admin' }
+ ]
+ });
+
+ switch (action) {
+ case 'set-password':
+ await setAdminPassword();
+ break;
+ case 'create-admin':
+ await createInitialAdmin();
+ break;
+ case 'reset-password':
+ await resetAdminPassword();
+ break;
+ case 'view-admin':
+ await viewAdminInfo();
+ break;
+ }
+
+ await redis.disconnect();
+ });
+
+// 🔑 API Key 管理
+program
+ .command('keys')
+ .description('API Key 管理')
+ .action(async () => {
+ await initialize();
+
+ // 尝试兼容不同版本的inquirer
+ let prompt = inquirer.prompt || inquirer.default?.prompt || inquirer;
+ if (typeof prompt !== 'function') {
+ prompt = (await import('inquirer')).default;
+ }
+
+ const { action } = await prompt({
+ type: 'list',
+ name: 'action',
+ message: '选择操作:',
+ choices: [
+ { name: '📋 列出所有 API Keys', value: 'list' },
+ { name: '🔑 创建新的 API Key', value: 'create' },
+ { name: '📝 更新 API Key', value: 'update' },
+ { name: '🗑️ 删除 API Key', value: 'delete' },
+ { name: '📊 查看使用统计', value: 'stats' },
+ { name: '🧹 重置所有统计数据', value: 'reset-stats' }
+ ]
+ });
+
+ switch (action) {
+ case 'list':
+ await listApiKeys();
+ break;
+ case 'create':
+ await createApiKey();
+ break;
+ case 'update':
+ await updateApiKey();
+ break;
+ case 'delete':
+ await deleteApiKey();
+ break;
+ case 'stats':
+ await viewApiKeyStats();
+ break;
+ case 'reset-stats':
+ await resetAllApiKeyStats();
+ break;
+ }
+
+ await redis.disconnect();
+ });
+
+// 🏢 Claude 账户管理
+program
+ .command('accounts')
+ .description('Claude 账户管理')
+ .action(async () => {
+ await initialize();
+
+ const { action } = await inquirer.prompt({
+ type: 'list',
+ name: 'action',
+ message: '选择操作:',
+ choices: [
+ { name: '📋 列出所有账户', value: 'list' },
+ { name: '🏢 创建新账户', value: 'create' },
+ { name: '📝 更新账户', value: 'update' },
+ { name: '🗑️ 删除账户', value: 'delete' },
+ { name: '🔄 刷新 Token', value: 'refresh' },
+ { name: '🧪 测试账户', value: 'test' }
+ ]
+ });
+
+ switch (action) {
+ case 'list':
+ await listClaudeAccounts();
+ break;
+ case 'create':
+ await createClaudeAccount();
+ break;
+ case 'update':
+ await updateClaudeAccount();
+ break;
+ case 'delete':
+ await deleteClaudeAccount();
+ break;
+ case 'refresh':
+ await refreshAccountToken();
+ break;
+ case 'test':
+ await testClaudeAccount();
+ break;
+ }
+
+ await redis.disconnect();
+ });
+
+// 🧹 重置统计数据命令
+program
+ .command('reset-stats')
+ .description('重置所有API Key的统计数据')
+ .option('--force', '跳过确认直接重置')
+ .option('--debug', '显示详细的Redis键调试信息')
+ .action(async (options) => {
+ await initialize();
+
+ console.log(styles.title('\n🧹 重置所有API Key统计数据\n'));
+
+ // 如果启用调试,显示当前Redis键
+ if (options.debug) {
+ console.log(styles.info('🔍 调试模式: 检查Redis中的实际键...\n'));
+ try {
+ const usageKeys = await redis.getClient().keys('usage:*');
+ const apiKeyKeys = await redis.getClient().keys('apikey:*');
+
+ console.log(styles.dim('API Key 键:'));
+ apiKeyKeys.forEach(key => console.log(` ${key}`));
+
+ console.log(styles.dim('\nUsage 键:'));
+ usageKeys.forEach(key => console.log(` ${key}`));
+
+ // 检查今日统计键
+ const today = new Date().toISOString().split('T')[0];
+ const dailyKeys = await redis.getClient().keys(`usage:daily:*:${today}`);
+ console.log(styles.dim(`\n今日统计键 (${today}):`));
+ dailyKeys.forEach(key => console.log(` ${key}`));
+
+ console.log('');
+ } catch (error) {
+ console.error(styles.error('调试信息获取失败:', error.message));
+ }
+ }
+
+ // 显示警告信息
+ console.log(styles.warning('⚠️ 警告: 此操作将删除所有API Key的使用统计数据!'));
+ console.log(styles.dim(' 包括: Token使用量、请求数量、每日/每月统计、最后使用时间等'));
+ console.log(styles.dim(' 此操作不可逆,请谨慎操作!\n'));
+
+ if (!options.force) {
+ console.log(styles.info('如需强制执行,请使用: npm run cli reset-stats -- --force\n'));
+ console.log(styles.error('操作已取消 - 请添加 --force 参数确认重置'));
+ await redis.disconnect();
+ return;
+ }
+
+ // 获取当前统计概览
+ const spinner = ora('正在获取当前统计数据...').start();
+ try {
+ const apiKeys = await apiKeyService.getAllApiKeys();
+ const totalTokens = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
+ const totalRequests = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
+
+ spinner.succeed('统计数据获取完成');
+
+ console.log(styles.info('\n📊 当前统计概览:'));
+ console.log(` API Keys 数量: ${apiKeys.length}`);
+ console.log(` 总 Token 使用量: ${totalTokens.toLocaleString()}`);
+ console.log(` 总请求数量: ${totalRequests.toLocaleString()}\n`);
+
+ // 执行重置操作
+ const resetSpinner = ora('正在重置所有API Key统计数据...').start();
+
+ const stats = await redis.resetAllUsageStats();
+
+ resetSpinner.succeed('所有统计数据重置完成');
+
+ // 显示重置结果
+ console.log(styles.success('\n✅ 重置操作完成!\n'));
+ console.log(styles.info('📊 重置详情:'));
+ console.log(` 重置的API Key数量: ${stats.resetApiKeys}`);
+ console.log(` 删除的总体统计: ${stats.deletedKeys} 个`);
+ console.log(` 删除的每日统计: ${stats.deletedDailyKeys} 个`);
+ console.log(` 删除的每月统计: ${stats.deletedMonthlyKeys} 个`);
+
+ console.log(styles.warning('\n💡 提示: API Key本身未被删除,只是清空了使用统计数据'));
+
+ } catch (error) {
+ spinner.fail('重置操作失败');
+ console.error(styles.error(error.message));
+ }
+
+ await redis.disconnect();
+ });
+
+// 📊 系统状态
+program
+ .command('status')
+ .description('查看系统状态')
+ .action(async () => {
+ await initialize();
+
+ const spinner = ora('正在获取系统状态...').start();
+
+ try {
+ const [systemStats, apiKeys, accounts] = await Promise.all([
+ redis.getSystemStats(),
+ apiKeyService.getAllApiKeys(),
+ claudeAccountService.getAllAccounts()
+ ]);
+
+ spinner.succeed('系统状态获取成功');
+
+ console.log(styles.title('\n📊 系统状态概览\n'));
+
+ const statusData = [
+ ['项目', '数量', '状态'],
+ ['API Keys', apiKeys.length, `${apiKeys.filter(k => k.isActive).length} 活跃`],
+ ['Claude 账户', accounts.length, `${accounts.filter(a => a.isActive).length} 活跃`],
+ ['Redis 连接', redis.isConnected ? '已连接' : '未连接', redis.isConnected ? '🟢' : '🔴'],
+ ['运行时间', `${Math.floor(process.uptime() / 60)} 分钟`, '🕐']
+ ];
+
+ console.log(table(statusData));
+
+ // 使用统计
+ const totalTokens = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
+ const totalRequests = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
+
+ console.log(styles.title('\n📈 使用统计\n'));
+ console.log(`总 Token 使用量: ${styles.success(totalTokens.toLocaleString())}`);
+ console.log(`总请求数: ${styles.success(totalRequests.toLocaleString())}`);
+
+ } catch (error) {
+ spinner.fail('获取系统状态失败');
+ console.error(styles.error(error.message));
+ }
+
+ await redis.disconnect();
+ });
+
+// 🧹 清理命令
+program
+ .command('cleanup')
+ .description('清理过期数据')
+ .action(async () => {
+ await initialize();
+
+ const { confirm } = await inquirer.prompt({
+ type: 'confirm',
+ name: 'confirm',
+ message: '确定要清理过期数据吗?',
+ default: false
+ });
+
+ if (!confirm) {
+ console.log(styles.warning('操作已取消'));
+ await redis.disconnect();
+ return;
+ }
+
+ const spinner = ora('正在清理过期数据...').start();
+
+ try {
+ const [expiredKeys, errorAccounts] = await Promise.all([
+ apiKeyService.cleanupExpiredKeys(),
+ claudeAccountService.cleanupErrorAccounts()
+ ]);
+
+ await redis.cleanup();
+
+ spinner.succeed('清理完成');
+ console.log(`${styles.success('✅')} 清理了 ${expiredKeys} 个过期 API Key`);
+ console.log(`${styles.success('✅')} 重置了 ${errorAccounts} 个错误账户`);
+
+ } catch (error) {
+ spinner.fail('清理失败');
+ console.error(styles.error(error.message));
+ }
+
+ await redis.disconnect();
+ });
+
+// 实现具体功能函数
+
+async function createInitialAdmin() {
+ console.log(styles.title('\n🔐 创建初始管理员账户\n'));
+
+ const adminData = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'username',
+ message: '用户名:',
+ default: 'admin',
+ validate: input => input.length >= 3 || '用户名至少3个字符'
+ },
+ {
+ type: 'password',
+ name: 'password',
+ message: '密码:',
+ validate: input => input.length >= 8 || '密码至少8个字符'
+ },
+ {
+ type: 'password',
+ name: 'confirmPassword',
+ message: '确认密码:',
+ validate: (input, answers) => input === answers.password || '密码不匹配'
+ }
+ ]);
+
+ const spinner = ora('正在创建管理员账户...').start();
+
+ try {
+ const passwordHash = await bcrypt.hash(adminData.password, 12);
+
+ const credentials = {
+ username: adminData.username,
+ passwordHash,
+ createdAt: new Date().toISOString(),
+ id: crypto.randomBytes(16).toString('hex')
+ };
+
+ await redis.setSession('admin_credentials', credentials, 0); // 永不过期
+
+ spinner.succeed('管理员账户创建成功');
+ console.log(`${styles.success('✅')} 用户名: ${adminData.username}`);
+ console.log(`${styles.info('ℹ️')} 请妥善保管登录凭据`);
+
+ } catch (error) {
+ spinner.fail('创建管理员账户失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function setAdminPassword() {
+ console.log(styles.title('\n🔑 设置管理员密码\n'));
+
+ const passwordData = await inquirer.prompt([
+ {
+ type: 'password',
+ name: 'newPassword',
+ message: '新密码:',
+ validate: input => input.length >= 8 || '密码至少8个字符'
+ },
+ {
+ type: 'password',
+ name: 'confirmPassword',
+ message: '确认密码:',
+ validate: (input, answers) => input === answers.newPassword || '密码不匹配'
+ }
+ ]);
+
+ const spinner = ora('正在更新密码...').start();
+
+ try {
+ const adminData = await redis.getSession('admin_credentials');
+
+ if (!adminData || Object.keys(adminData).length === 0) {
+ spinner.fail('未找到管理员账户');
+ console.log(styles.warning('请先创建初始管理员账户'));
+ return;
+ }
+
+ const passwordHash = await bcrypt.hash(passwordData.newPassword, 12);
+ adminData.passwordHash = passwordHash;
+ adminData.updatedAt = new Date().toISOString();
+
+ await redis.setSession('admin_credentials', adminData, 0);
+
+ spinner.succeed('密码更新成功');
+ console.log(`${styles.success('✅')} 管理员密码已更新`);
+
+ } catch (error) {
+ spinner.fail('密码更新失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function listApiKeys() {
+ const spinner = ora('正在获取 API Keys...').start();
+
+ try {
+ const apiKeys = await apiKeyService.getAllApiKeys();
+ spinner.succeed(`找到 ${apiKeys.length} 个 API Key`);
+
+ if (apiKeys.length === 0) {
+ console.log(styles.warning('没有找到任何 API Key'));
+ return;
+ }
+
+ const tableData = [
+ ['ID', '名称', '状态', 'Token使用', '请求数', '创建时间']
+ ];
+
+ apiKeys.forEach(key => {
+ tableData.push([
+ key.id.substring(0, 8) + '...',
+ key.name,
+ key.isActive ? '🟢 活跃' : '🔴 停用',
+ key.usage?.total?.tokens?.toLocaleString() || '0',
+ key.usage?.total?.requests?.toLocaleString() || '0',
+ new Date(key.createdAt).toLocaleDateString()
+ ]);
+ });
+
+ console.log('\n📋 API Keys 列表:\n');
+ console.log(table(tableData));
+
+ } catch (error) {
+ spinner.fail('获取 API Keys 失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function createApiKey() {
+ console.log(styles.title('\n🔑 创建新的 API Key\n'));
+
+ const keyData = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'name',
+ message: 'API Key 名称:',
+ validate: input => input.length > 0 || '名称不能为空'
+ },
+ {
+ type: 'input',
+ name: 'description',
+ message: '描述 (可选):'
+ },
+ {
+ type: 'number',
+ name: 'tokenLimit',
+ message: 'Token 限制 (0=无限制):',
+ default: 1000000
+ },
+ {
+ type: 'number',
+ name: 'requestLimit',
+ message: '请求限制 (0=无限制):',
+ default: 1000
+ }
+ ]);
+
+ const spinner = ora('正在创建 API Key...').start();
+
+ try {
+ const newKey = await apiKeyService.generateApiKey(keyData);
+
+ spinner.succeed('API Key 创建成功');
+ console.log(`${styles.success('✅')} API Key: ${styles.warning(newKey.apiKey)}`);
+ console.log(`${styles.info('ℹ️')} 请妥善保管此 API Key,它只会显示一次`);
+
+ } catch (error) {
+ spinner.fail('创建 API Key 失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function resetAllApiKeyStats() {
+ console.log(styles.title('\n🧹 重置所有API Key统计数据\n'));
+
+ // 显示警告信息
+ console.log(styles.warning('⚠️ 警告: 此操作将删除所有API Key的使用统计数据!'));
+ console.log(styles.dim(' 包括: Token使用量、请求数量、每日/每月统计、最后使用时间等'));
+ console.log(styles.dim(' 此操作不可逆,请谨慎操作!\n'));
+
+ // 第一次确认
+ const { firstConfirm } = await inquirer.prompt({
+ type: 'confirm',
+ name: 'firstConfirm',
+ message: '您确定要重置所有API Key的统计数据吗?',
+ default: false
+ });
+
+ if (!firstConfirm) {
+ console.log(styles.info('操作已取消'));
+ return;
+ }
+
+ // 获取当前统计概览
+ const spinner = ora('正在获取当前统计数据...').start();
+ try {
+ const apiKeys = await apiKeyService.getAllApiKeys();
+ const totalTokens = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
+ const totalRequests = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
+
+ spinner.succeed('统计数据获取完成');
+
+ console.log(styles.info('\n📊 当前统计概览:'));
+ console.log(` API Keys 数量: ${apiKeys.length}`);
+ console.log(` 总 Token 使用量: ${totalTokens.toLocaleString()}`);
+ console.log(` 总请求数量: ${totalRequests.toLocaleString()}\n`);
+
+ // 第二次确认(需要输入"RESET")
+ const { confirmation } = await inquirer.prompt({
+ type: 'input',
+ name: 'confirmation',
+ message: '请输入 "RESET" 来确认重置操作:',
+ validate: input => input === 'RESET' || '请输入正确的确认文本 "RESET"'
+ });
+
+ if (confirmation !== 'RESET') {
+ console.log(styles.info('操作已取消'));
+ return;
+ }
+
+ // 执行重置操作
+ const resetSpinner = ora('正在重置所有API Key统计数据...').start();
+
+ const stats = await redis.resetAllUsageStats();
+
+ resetSpinner.succeed('所有统计数据重置完成');
+
+ // 显示重置结果
+ console.log(styles.success('\n✅ 重置操作完成!\n'));
+ console.log(styles.info('📊 重置详情:'));
+ console.log(` 重置的API Key数量: ${stats.resetApiKeys}`);
+ console.log(` 删除的总体统计: ${stats.deletedKeys} 个`);
+ console.log(` 删除的每日统计: ${stats.deletedDailyKeys} 个`);
+ console.log(` 删除的每月统计: ${stats.deletedMonthlyKeys} 个`);
+
+ console.log(styles.warning('\n💡 提示: API Key本身未被删除,只是清空了使用统计数据'));
+
+ } catch (error) {
+ spinner.fail('重置操作失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function viewApiKeyStats() {
+ console.log(styles.title('\n📊 API Key 使用统计\n'));
+
+ const spinner = ora('正在获取统计数据...').start();
+
+ try {
+ const apiKeys = await apiKeyService.getAllApiKeys();
+
+ if (apiKeys.length === 0) {
+ spinner.succeed('获取完成');
+ console.log(styles.warning('没有找到任何 API Key'));
+ return;
+ }
+
+ spinner.succeed(`找到 ${apiKeys.length} 个 API Key 的统计数据`);
+
+ const tableData = [
+ ['名称', 'Token总量', '输入Token', '输出Token', '请求数', '最后使用']
+ ];
+
+ let totalTokens = 0;
+ let totalRequests = 0;
+
+ apiKeys.forEach(key => {
+ const usage = key.usage?.total || {};
+ const tokens = usage.tokens || 0;
+ const inputTokens = usage.inputTokens || 0;
+ const outputTokens = usage.outputTokens || 0;
+ const requests = usage.requests || 0;
+
+ totalTokens += tokens;
+ totalRequests += requests;
+
+ tableData.push([
+ key.name,
+ tokens.toLocaleString(),
+ inputTokens.toLocaleString(),
+ outputTokens.toLocaleString(),
+ requests.toLocaleString(),
+ key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : '从未使用'
+ ]);
+ });
+
+ console.log(table(tableData));
+
+ console.log(styles.info('\n📈 总计统计:'));
+ console.log(`总 Token 使用量: ${styles.success(totalTokens.toLocaleString())}`);
+ console.log(`总请求数量: ${styles.success(totalRequests.toLocaleString())}`);
+
+ } catch (error) {
+ spinner.fail('获取统计数据失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function updateApiKey() {
+ console.log(styles.title('\n📝 更新 API Key\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function deleteApiKey() {
+ console.log(styles.title('\n🗑️ 删除 API Key\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function resetAdminPassword() {
+ console.log(styles.title('\n🔄 重置管理员密码\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function viewAdminInfo() {
+ console.log(styles.title('\n👤 管理员信息\n'));
+
+ const spinner = ora('正在获取管理员信息...').start();
+
+ try {
+ const adminData = await redis.getSession('admin_credentials');
+
+ if (!adminData || Object.keys(adminData).length === 0) {
+ spinner.fail('未找到管理员账户');
+ console.log(styles.warning('请先创建初始管理员账户'));
+ return;
+ }
+
+ spinner.succeed('管理员信息获取成功');
+
+ console.log(`用户名: ${styles.info(adminData.username)}`);
+ console.log(`创建时间: ${styles.dim(new Date(adminData.createdAt).toLocaleString())}`);
+ console.log(`最后登录: ${adminData.lastLogin ? styles.dim(new Date(adminData.lastLogin).toLocaleString()) : '从未登录'}`);
+
+ } catch (error) {
+ spinner.fail('获取管理员信息失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+async function createClaudeAccount() {
+ console.log(styles.title('\n🏢 创建 Claude 账户\n'));
+ console.log(styles.warning('功能开发中... 请使用Web界面创建OAuth账户'));
+}
+
+async function updateClaudeAccount() {
+ console.log(styles.title('\n📝 更新 Claude 账户\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function deleteClaudeAccount() {
+ console.log(styles.title('\n🗑️ 删除 Claude 账户\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function refreshAccountToken() {
+ console.log(styles.title('\n🔄 刷新账户 Token\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function testClaudeAccount() {
+ console.log(styles.title('\n🧪 测试 Claude 账户\n'));
+ console.log(styles.warning('功能开发中...'));
+}
+
+async function listClaudeAccounts() {
+ const spinner = ora('正在获取 Claude 账户...').start();
+
+ try {
+ const accounts = await claudeAccountService.getAllAccounts();
+ spinner.succeed(`找到 ${accounts.length} 个 Claude 账户`);
+
+ if (accounts.length === 0) {
+ console.log(styles.warning('没有找到任何 Claude 账户'));
+ return;
+ }
+
+ const tableData = [
+ ['ID', '名称', '邮箱', '状态', '代理', '最后使用']
+ ];
+
+ accounts.forEach(account => {
+ tableData.push([
+ account.id.substring(0, 8) + '...',
+ account.name,
+ account.email || '-',
+ account.isActive ? (account.status === 'active' ? '🟢 活跃' : '🟡 待激活') : '🔴 停用',
+ account.proxy ? '🌐 是' : '-',
+ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '-'
+ ]);
+ });
+
+ console.log('\n🏢 Claude 账户列表:\n');
+ console.log(table(tableData));
+
+ } catch (error) {
+ spinner.fail('获取 Claude 账户失败');
+ console.error(styles.error(error.message));
+ }
+}
+
+// 程序信息
+program
+ .name('claude-relay-cli')
+ .description('Claude Relay Service 命令行管理工具')
+ .version('1.0.0');
+
+// 解析命令行参数
+program.parse();
+
+// 如果没有提供命令,显示帮助
+if (!process.argv.slice(2).length) {
+ console.log(styles.title('🚀 Claude Relay Service CLI\n'));
+ console.log('使用以下命令管理服务:\n');
+ console.log(' claude-relay-cli admin - 管理员账户操作');
+ console.log(' claude-relay-cli keys - API Key 管理 (包含重置统计数据)');
+ console.log(' claude-relay-cli accounts - Claude 账户管理');
+ console.log(' claude-relay-cli status - 查看系统状态');
+ console.log(' claude-relay-cli cleanup - 清理过期数据');
+ console.log(' claude-relay-cli reset-stats - 重置所有API Key统计数据');
+ console.log('\n使用 --help 查看详细帮助信息');
+}
\ No newline at end of file
diff --git a/config/config.example.js b/config/config.example.js
new file mode 100644
index 00000000..166f643a
--- /dev/null
+++ b/config/config.example.js
@@ -0,0 +1,90 @@
+const path = require('path');
+require('dotenv').config();
+
+const config = {
+ // 🌐 服务器配置
+ server: {
+ port: parseInt(process.env.PORT) || 3000,
+ host: process.env.HOST || '0.0.0.0',
+ nodeEnv: process.env.NODE_ENV || 'development',
+ trustProxy: process.env.TRUST_PROXY === 'true'
+ },
+
+ // 🔐 安全配置
+ security: {
+ jwtSecret: process.env.JWT_SECRET || 'CHANGE-THIS-JWT-SECRET-IN-PRODUCTION',
+ adminSessionTimeout: parseInt(process.env.ADMIN_SESSION_TIMEOUT) || 86400000, // 24小时
+ apiKeyPrefix: process.env.API_KEY_PREFIX || 'cr_',
+ encryptionKey: process.env.ENCRYPTION_KEY || 'CHANGE-THIS-32-CHARACTER-KEY-NOW'
+ },
+
+ // 📊 Redis配置
+ redis: {
+ host: process.env.REDIS_HOST || '127.0.0.1',
+ port: parseInt(process.env.REDIS_PORT) || 6379,
+ password: process.env.REDIS_PASSWORD || '',
+ db: parseInt(process.env.REDIS_DB) || 0,
+ connectTimeout: 10000,
+ commandTimeout: 5000,
+ retryDelayOnFailover: 100,
+ maxRetriesPerRequest: 3,
+ lazyConnect: true
+ },
+
+ // 🎯 Claude API配置
+ claude: {
+ apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
+ apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01',
+ betaHeader: process.env.CLAUDE_BETA_HEADER || 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
+ },
+
+ // 🌐 代理配置
+ proxy: {
+ timeout: parseInt(process.env.DEFAULT_PROXY_TIMEOUT) || 30000,
+ maxRetries: parseInt(process.env.MAX_PROXY_RETRIES) || 3
+ },
+
+ // 📈 使用限制
+ limits: {
+ defaultTokenLimit: parseInt(process.env.DEFAULT_TOKEN_LIMIT) || 1000000,
+ defaultRequestLimit: parseInt(process.env.DEFAULT_REQUEST_LIMIT) || 1000
+ },
+
+ // 🚦 速率限制
+ rateLimit: {
+ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW) || 60000,
+ maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100
+ },
+
+ // 📝 日志配置
+ logging: {
+ level: process.env.LOG_LEVEL || 'info',
+ dirname: path.join(__dirname, '..', 'logs'),
+ maxSize: process.env.LOG_MAX_SIZE || '10m',
+ maxFiles: parseInt(process.env.LOG_MAX_FILES) || 5
+ },
+
+ // 🔧 系统配置
+ system: {
+ cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL) || 3600000, // 1小时
+ tokenUsageRetention: parseInt(process.env.TOKEN_USAGE_RETENTION) || 2592000000, // 30天
+ healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL) || 60000 // 1分钟
+ },
+
+ // 🎨 Web界面配置
+ web: {
+ title: process.env.WEB_TITLE || 'Claude Relay Service',
+ description: process.env.WEB_DESCRIPTION || 'Multi-account Claude API relay service with beautiful management interface',
+ logoUrl: process.env.WEB_LOGO_URL || '/assets/logo.png',
+ enableCors: process.env.ENABLE_CORS === 'true',
+ sessionSecret: process.env.WEB_SESSION_SECRET || 'CHANGE-THIS-SESSION-SECRET'
+ },
+
+ // 🛠️ 开发配置
+ development: {
+ debug: process.env.DEBUG === 'true',
+ hotReload: process.env.HOT_RELOAD === 'true'
+ }
+};
+
+module.exports = config;
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..1978f786
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,117 @@
+version: '3.8'
+
+services:
+ # 🚀 Claude Relay Service
+ claude-relay:
+ build: .
+ container_name: claude-relay-service
+ restart: unless-stopped
+ ports:
+ - "${PORT:-3000}:3000"
+ environment:
+ - NODE_ENV=production
+ - PORT=3000
+ - REDIS_HOST=redis
+ - REDIS_PORT=6379
+ volumes:
+ - ./logs:/app/logs
+ - ./data:/app/data
+ depends_on:
+ - redis
+ networks:
+ - claude-relay-network
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ # 📊 Redis Database
+ redis:
+ image: redis:7-alpine
+ container_name: claude-relay-redis
+ restart: unless-stopped
+ ports:
+ - "${REDIS_PORT:-6379}:6379"
+ volumes:
+ - redis_data:/data
+ - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro
+ command: redis-server /usr/local/etc/redis/redis.conf
+ networks:
+ - claude-relay-network
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ # 📈 Redis Monitoring (Optional)
+ redis-commander:
+ image: rediscommander/redis-commander:latest
+ container_name: claude-relay-redis-web
+ restart: unless-stopped
+ ports:
+ - "${REDIS_WEB_PORT:-8081}:8081"
+ environment:
+ - REDIS_HOSTS=local:redis:6379
+ depends_on:
+ - redis
+ networks:
+ - claude-relay-network
+ profiles:
+ - monitoring
+
+ # 📊 Application Monitoring (Optional)
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: claude-relay-prometheus
+ restart: unless-stopped
+ ports:
+ - "${PROMETHEUS_PORT:-9090}:9090"
+ volumes:
+ - ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - prometheus_data:/prometheus
+ command:
+ - '--config.file=/etc/prometheus/prometheus.yml'
+ - '--storage.tsdb.path=/prometheus'
+ - '--web.console.libraries=/etc/prometheus/console_libraries'
+ - '--web.console.templates=/etc/prometheus/consoles'
+ - '--web.enable-lifecycle'
+ networks:
+ - claude-relay-network
+ profiles:
+ - monitoring
+
+ # 📈 Grafana Dashboard (Optional)
+ grafana:
+ image: grafana/grafana:latest
+ container_name: claude-relay-grafana
+ restart: unless-stopped
+ ports:
+ - "${GRAFANA_PORT:-3001}:3000"
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin123}
+ volumes:
+ - grafana_data:/var/lib/grafana
+ - ./config/grafana:/etc/grafana/provisioning
+ depends_on:
+ - prometheus
+ networks:
+ - claude-relay-network
+ profiles:
+ - monitoring
+
+volumes:
+ redis_data:
+ driver: local
+ prometheus_data:
+ driver: local
+ grafana_data:
+ driver: local
+
+networks:
+ claude-relay-network:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.20.0.0/16
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..73cb01e9
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,7555 @@
+{
+ "name": "claude-relay-service",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "claude-relay-service",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.0",
+ "bcryptjs": "^2.4.3",
+ "chalk": "^4.1.2",
+ "commander": "^11.1.0",
+ "compression": "^1.7.4",
+ "cors": "^2.8.5",
+ "crypto": "^1.0.1",
+ "dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "express-validator": "^7.0.1",
+ "helmet": "^7.1.0",
+ "https-proxy-agent": "^7.0.2",
+ "inquirer": "^9.2.15",
+ "ioredis": "^5.3.2",
+ "jsonwebtoken": "^9.0.2",
+ "morgan": "^1.10.0",
+ "multer": "^1.4.5-lts.1",
+ "node-cron": "^3.0.3",
+ "ora": "^5.4.1",
+ "rate-limiter-flexible": "^5.0.5",
+ "redis": "^4.6.10",
+ "socks-proxy-agent": "^8.0.2",
+ "table": "^6.8.1",
+ "uuid": "^9.0.1",
+ "winston": "^3.11.0",
+ "winston-daily-rotate-file": "^4.7.1"
+ },
+ "devDependencies": {
+ "@types/node": "^20.8.9",
+ "eslint": "^8.53.0",
+ "jest": "^29.7.0",
+ "nodemon": "^3.0.1",
+ "supertest": "^6.3.3"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.0.tgz",
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.0.tgz",
+ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.6",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/core/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.0.tgz",
+ "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.27.6.tgz",
+ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.0.tgz",
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.0.tgz",
+ "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/traverse/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.0.tgz",
+ "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@colors/colors": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/@colors/colors/-/colors-1.6.0.tgz",
+ "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@dabh/diagnostics": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+ "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+ "license": "MIT",
+ "dependencies": {
+ "colorspace": "1.1.x",
+ "enabled": "2.0.x",
+ "kuler": "^2.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/eslintrc/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmmirror.com/@inquirer/figures/-/figures-1.0.12.tgz",
+ "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ioredis/commands": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.2.0.tgz",
+ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
+ "license": "MIT"
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/console/-/console-29.7.0.tgz",
+ "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/core/-/core-29.7.0.tgz",
+ "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/reporters": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^29.7.0",
+ "jest-config": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-resolve-dependencies": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/environment/-/environment-29.7.0.tgz",
+ "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+ "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
+ "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@types/node": "*",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/globals/-/globals-29.7.0.tgz",
+ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/reporters/-/reporters-29.7.0.tgz",
+ "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/@jest/source-map/-/source-map-29.6.3.tgz",
+ "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/test-result/-/test-result-29.7.0.tgz",
+ "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz",
+ "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/@jest/transform/-/transform-29.7.0.tgz",
+ "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^29.6.3",
+ "@jridgewell/trace-mapping": "^0.3.18",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/@jest/types/-/types-29.6.3.tgz",
+ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
+ "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
+ "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.29",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
+ "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@paralleldrive/cuid2": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmmirror.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
+ "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.1.5"
+ }
+ },
+ "node_modules/@redis/bloom": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/@redis/bloom/-/bloom-1.2.0.tgz",
+ "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/client": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmmirror.com/@redis/client/-/client-1.6.1.tgz",
+ "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
+ "license": "MIT",
+ "dependencies": {
+ "cluster-key-slot": "1.1.2",
+ "generic-pool": "3.9.0",
+ "yallist": "4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@redis/client/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"
+ },
+ "node_modules/@redis/graph": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@redis/graph/-/graph-1.1.1.tgz",
+ "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/json": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/@redis/json/-/json-1.0.7.tgz",
+ "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/search": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/@redis/search/-/search-1.2.0.tgz",
+ "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/time-series": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@redis/time-series/-/time-series-1.1.0.tgz",
+ "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmmirror.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
+ "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+ "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.7",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.7.tgz",
+ "integrity": "sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/triple-beam": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmmirror.com/@types/triple-beam/-/triple-beam-1.3.5.tgz",
+ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.33",
+ "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.33.tgz",
+ "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "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/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "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/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-1.10.0.tgz",
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/babel-jest/-/babel-jest-29.7.0.tgz",
+ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
+ "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.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==",
+ "dev": true,
+ "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/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/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/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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.3",
+ "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.1",
+ "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.1.tgz",
+ "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001726",
+ "electron-to-chromium": "^1.5.173",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "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",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001727",
+ "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
+ "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmmirror.com/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "license": "MIT"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
+ "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmmirror.com/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz",
+ "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.3",
+ "color-string": "^1.6.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/color/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "license": "MIT"
+ },
+ "node_modules/colorspace": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/colorspace/-/colorspace-1.1.4.tgz",
+ "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+ "license": "MIT",
+ "dependencies": {
+ "color": "^3.1.3",
+ "text-hex": "1.0.x"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/compression/-/compression-1.8.0.tgz",
+ "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "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/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
+ "dev": true,
+ "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/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/create-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/create-jest/-/create-jest-29.7.0.tgz",
+ "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "prompts": "^2.0.1"
+ },
+ "bin": {
+ "create-jest": "bin/create-jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz",
+ "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
+ "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
+ "license": "ISC"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/dedent/-/dedent-1.6.0.tgz",
+ "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "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/electron-to-chromium": {
+ "version": "1.5.182",
+ "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz",
+ "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmmirror.com/emittery/-/emittery-0.13.1.tgz",
+ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "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/enabled": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz",
+ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-validator": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmmirror.com/express-validator/-/express-validator-7.2.1.tgz",
+ "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "validator": "~13.12.0"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.0.6.tgz",
+ "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/fecha": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz",
+ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-stream-rotator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmmirror.com/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
+ "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.29.1"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fn.name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/fn.name/-/fn.name-1.1.0.tgz",
+ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz",
+ "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/formidable": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmmirror.com/formidable/-/formidable-2.1.5.tgz",
+ "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@paralleldrive/cuid2": "^2.2.2",
+ "dezalgo": "^1.0.4",
+ "once": "^1.4.0",
+ "qs": "^6.11.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generic-pool": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmmirror.com/generic-pool/-/generic-pool-3.9.0.tgz",
+ "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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",
+ "dev": true,
+ "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/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz",
+ "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "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/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "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/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "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.",
+ "dev": true,
+ "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/inquirer": {
+ "version": "9.3.7",
+ "resolved": "https://registry.npmmirror.com/inquirer/-/inquirer-9.3.7.tgz",
+ "integrity": "sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/figures": "^1.0.3",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "external-editor": "^3.1.0",
+ "mute-stream": "1.0.0",
+ "ora": "^5.4.1",
+ "run-async": "^3.0.0",
+ "rxjs": "^7.8.1",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ioredis": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmmirror.com/ioredis/-/ioredis-5.6.1.tgz",
+ "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@ioredis/commands": "^1.1.1",
+ "cluster-key-slot": "^1.1.0",
+ "debug": "^4.3.4",
+ "denque": "^2.1.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.isarguments": "^3.1.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0",
+ "standard-as-callback": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
+ "node_modules/ioredis/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ioredis/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/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "license": "MIT",
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest/-/jest-29.7.0.tgz",
+ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "import-local": "^3.0.2",
+ "jest-cli": "^29.7.0"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
+ "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^5.0.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-circus/-/jest-circus-29.7.0.tgz",
+ "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^1.0.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^29.7.0",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^29.7.0",
+ "pure-rand": "^6.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-cli/-/jest-cli-29.7.0.tgz",
+ "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "create-jest": "^29.7.0",
+ "exit": "^0.1.2",
+ "import-local": "^3.0.2",
+ "jest-config": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-config/-/jest-config-29.7.0.tgz",
+ "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-jest": "^29.7.0",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-runner": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-diff/-/jest-diff-29.7.0.tgz",
+ "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-docblock/-/jest-docblock-29.7.0.tgz",
+ "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-each/-/jest-each-29.7.0.tgz",
+ "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
+ "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/jest-get-type/-/jest-get-type-29.6.3.tgz",
+ "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
+ "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
+ "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+ "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-message-util/-/jest-message-util-29.7.0.tgz",
+ "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^29.6.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^29.7.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-mock/-/jest-mock-29.7.0.tgz",
+ "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmmirror.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
+ "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-resolve/-/jest-resolve-29.7.0.tgz",
+ "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^2.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz",
+ "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-regex-util": "^29.6.3",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-runner/-/jest-runner-29.7.0.tgz",
+ "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^29.7.0",
+ "@jest/environment": "^29.7.0",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^29.7.0",
+ "jest-environment-node": "^29.7.0",
+ "jest-haste-map": "^29.7.0",
+ "jest-leak-detector": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-resolve": "^29.7.0",
+ "jest-runtime": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jest-watcher": "^29.7.0",
+ "jest-worker": "^29.7.0",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-runtime/-/jest-runtime-29.7.0.tgz",
+ "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+ "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-util/-/jest-util-29.7.0.tgz",
+ "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-validate/-/jest-validate-29.7.0.tgz",
+ "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "leven": "^3.1.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-watcher/-/jest-watcher-29.7.0.tgz",
+ "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.13.1",
+ "jest-util": "^29.7.0",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-29.7.0.tgz",
+ "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-util": "^29.7.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "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/jsonwebtoken/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz",
+ "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
+ "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": "3.2.2",
+ "resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/kuler": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz",
+ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
+ "license": "MIT"
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "license": "MIT"
+ },
+ "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.isarguments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+ "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
+ "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.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "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/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/logform": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz",
+ "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@colors/colors": "1.6.0",
+ "@types/triple-beam": "^1.3.2",
+ "fecha": "^4.2.0",
+ "ms": "^2.1.1",
+ "safe-stable-stringify": "^2.3.1",
+ "triple-beam": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/logform/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/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmmirror.com/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "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/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/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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/mute-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-1.0.0.tgz",
+ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-cron": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-3.0.3.tgz",
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+ "license": "ISC",
+ "dependencies": {
+ "uuid": "8.3.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/node-cron/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.10.tgz",
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nodemon/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nodemon/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/nodemon/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/one-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/one-time/-/one-time-1.0.0.tgz",
+ "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fn.name": "1.x.x"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "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/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "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/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/rate-limiter-flexible": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmmirror.com/rate-limiter-flexible/-/rate-limiter-flexible-5.0.5.tgz",
+ "integrity": "sha512-+/dSQfo+3FYwYygUs/V2BBdwGa9nFtakDwKt4l0bnvNB53TNT++QSFewwHX9qXrZJuMe9j+TUaU21lm5ARgqdQ==",
+ "license": "ISC"
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/redis": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmmirror.com/redis/-/redis-4.7.1.tgz",
+ "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==",
+ "license": "MIT",
+ "workspaces": [
+ "./packages/*"
+ ],
+ "dependencies": {
+ "@redis/bloom": "1.2.0",
+ "@redis/client": "1.6.1",
+ "@redis/graph": "1.1.1",
+ "@redis/json": "1.0.7",
+ "@redis/search": "1.2.0",
+ "@redis/time-series": "1.1.0"
+ }
+ },
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
+ "license": "MIT",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/resolve.exports/-/resolve.exports-2.0.3.tgz",
+ "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "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",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/run-async/-/run-async-3.0.0.tgz",
+ "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "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": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "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/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/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/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "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-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "license": "MIT"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.6.tgz",
+ "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socks-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/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmmirror.com/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "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-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/superagent": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
+ "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==",
+ "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "component-emitter": "^1.3.0",
+ "cookiejar": "^2.1.4",
+ "debug": "^4.3.4",
+ "fast-safe-stringify": "^2.1.1",
+ "form-data": "^4.0.0",
+ "formidable": "^2.1.2",
+ "methods": "^1.1.2",
+ "mime": "2.6.0",
+ "qs": "^6.11.0",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">=6.4.0 <13 || >=14"
+ }
+ },
+ "node_modules/superagent/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/superagent/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/superagent/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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/superagent/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/supertest": {
+ "version": "6.3.4",
+ "resolved": "https://registry.npmmirror.com/supertest/-/supertest-6.3.4.tgz",
+ "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==",
+ "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "methods": "^1.1.2",
+ "superagent": "^8.1.2"
+ },
+ "engines": {
+ "node": ">=6.4.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/table": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmmirror.com/table/-/table-6.9.0.tgz",
+ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-hex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/text-hex/-/text-hex-1.0.0.tgz",
+ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
+ "license": "MIT"
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "license": "MIT",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/triple-beam": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz",
+ "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "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/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "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",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmmirror.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.12.0",
+ "resolved": "https://registry.npmmirror.com/validator/-/validator-13.12.0.tgz",
+ "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/winston": {
+ "version": "3.17.0",
+ "resolved": "https://registry.npmmirror.com/winston/-/winston-3.17.0.tgz",
+ "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@colors/colors": "^1.6.0",
+ "@dabh/diagnostics": "^2.0.2",
+ "async": "^3.2.3",
+ "is-stream": "^2.0.0",
+ "logform": "^2.7.0",
+ "one-time": "^1.0.0",
+ "readable-stream": "^3.4.0",
+ "safe-stable-stringify": "^2.3.1",
+ "stack-trace": "0.0.x",
+ "triple-beam": "^1.3.0",
+ "winston-transport": "^4.9.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/winston-daily-rotate-file": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmmirror.com/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz",
+ "integrity": "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==",
+ "license": "MIT",
+ "dependencies": {
+ "file-stream-rotator": "^0.6.1",
+ "object-hash": "^2.0.1",
+ "triple-beam": "^1.3.0",
+ "winston-transport": "^4.4.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "winston": "^3"
+ }
+ },
+ "node_modules/winston-transport": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmmirror.com/winston-transport/-/winston-transport-4.9.0.tgz",
+ "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
+ "license": "MIT",
+ "dependencies": {
+ "logform": "^2.7.0",
+ "readable-stream": "^3.6.2",
+ "triple-beam": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/winston-transport/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/winston/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/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "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/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
+ "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..38c88325
--- /dev/null
+++ b/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "claude-relay-service",
+ "version": "1.0.0",
+ "description": "Claude Code API relay service with multi-account management and API key authentication",
+ "main": "src/app.js",
+ "scripts": {
+ "start": "node src/app.js",
+ "dev": "nodemon src/app.js",
+ "build:web": "cd web && npm run build",
+ "install:web": "cd web && npm install",
+ "setup": "node scripts/setup.js",
+ "cli": "node cli/index.js",
+ "service": "node scripts/manage.js",
+ "service:start": "node scripts/manage.js start",
+ "service:start:daemon": "node scripts/manage.js start -d",
+ "service:start:d": "node scripts/manage.js start -d",
+ "service:daemon": "node scripts/manage.js start -d",
+ "service:stop": "node scripts/manage.js stop",
+ "service:restart": "node scripts/manage.js restart",
+ "service:restart:daemon": "node scripts/manage.js restart -d",
+ "service:restart:d": "node scripts/manage.js restart -d",
+ "service:status": "node scripts/manage.js status",
+ "service:logs": "node scripts/manage.js logs",
+ "test": "jest",
+ "lint": "eslint src/**/*.js",
+ "docker:build": "docker build -t claude-relay-service .",
+ "docker:up": "docker-compose up -d",
+ "docker:down": "docker-compose down"
+ },
+ "dependencies": {
+ "axios": "^1.6.0",
+ "bcryptjs": "^2.4.3",
+ "chalk": "^4.1.2",
+ "commander": "^11.1.0",
+ "compression": "^1.7.4",
+ "cors": "^2.8.5",
+ "dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "helmet": "^7.1.0",
+ "https-proxy-agent": "^7.0.2",
+ "inquirer": "^9.2.15",
+ "ioredis": "^5.3.2",
+ "morgan": "^1.10.0",
+ "ora": "^5.4.1",
+ "rate-limiter-flexible": "^5.0.5",
+ "socks-proxy-agent": "^8.0.2",
+ "table": "^6.8.1",
+ "uuid": "^9.0.1",
+ "winston": "^3.11.0",
+ "winston-daily-rotate-file": "^4.7.1"
+ },
+ "devDependencies": {
+ "@types/node": "^20.8.9",
+ "eslint": "^8.53.0",
+ "jest": "^29.7.0",
+ "nodemon": "^3.0.1",
+ "supertest": "^6.3.3"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "keywords": [
+ "claude",
+ "api",
+ "proxy",
+ "relay",
+ "claude-code",
+ "anthropic"
+ ],
+ "author": "Claude Relay Service",
+ "license": "MIT"
+}
diff --git a/scripts/manage.js b/scripts/manage.js
new file mode 100644
index 00000000..5c4b9993
--- /dev/null
+++ b/scripts/manage.js
@@ -0,0 +1,335 @@
+#!/usr/bin/env node
+
+const { spawn, exec } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const process = require('process');
+
+const PID_FILE = path.join(__dirname, '..', 'claude-relay-service.pid');
+const LOG_FILE = path.join(__dirname, '..', 'logs', 'service.log');
+const ERROR_LOG_FILE = path.join(__dirname, '..', 'logs', 'service-error.log');
+const APP_FILE = path.join(__dirname, '..', 'src', 'app.js');
+
+class ServiceManager {
+ constructor() {
+ this.ensureLogDir();
+ }
+
+ ensureLogDir() {
+ const logDir = path.dirname(LOG_FILE);
+ if (!fs.existsSync(logDir)) {
+ fs.mkdirSync(logDir, { recursive: true });
+ }
+ }
+
+ getPid() {
+ try {
+ if (fs.existsSync(PID_FILE)) {
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
+ return pid;
+ }
+ } catch (error) {
+ console.error('读取PID文件失败:', error.message);
+ }
+ return null;
+ }
+
+ isProcessRunning(pid) {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch (error) {
+ return false;
+ }
+ }
+
+ writePid(pid) {
+ try {
+ fs.writeFileSync(PID_FILE, pid.toString());
+ console.log(`✅ PID ${pid} 已保存到 ${PID_FILE}`);
+ } catch (error) {
+ console.error('写入PID文件失败:', error.message);
+ }
+ }
+
+ removePidFile() {
+ try {
+ if (fs.existsSync(PID_FILE)) {
+ fs.unlinkSync(PID_FILE);
+ console.log('🗑️ 已清理PID文件');
+ }
+ } catch (error) {
+ console.error('清理PID文件失败:', error.message);
+ }
+ }
+
+ getStatus() {
+ const pid = this.getPid();
+ if (pid && this.isProcessRunning(pid)) {
+ return { running: true, pid };
+ }
+ return { running: false, pid: null };
+ }
+
+ start(daemon = false) {
+ const status = this.getStatus();
+ if (status.running) {
+ console.log(`⚠️ 服务已在运行中 (PID: ${status.pid})`);
+ return false;
+ }
+
+ console.log('🚀 启动 Claude Relay Service...');
+
+ if (daemon) {
+ // 后台运行模式 - 使用nohup实现真正的后台运行
+ const { exec } = require('child_process');
+
+ const command = `nohup node "${APP_FILE}" > "${LOG_FILE}" 2> "${ERROR_LOG_FILE}" & echo $!`;
+
+ exec(command, (error, stdout, stderr) => {
+ if (error) {
+ console.error('❌ 后台启动失败:', error.message);
+ return;
+ }
+
+ const pid = parseInt(stdout.trim());
+ if (pid && !isNaN(pid)) {
+ this.writePid(pid);
+ console.log(`🔄 服务已在后台启动 (PID: ${pid})`);
+ console.log(`📝 日志文件: ${LOG_FILE}`);
+ console.log(`❌ 错误日志: ${ERROR_LOG_FILE}`);
+ console.log('✅ 终端现在可以安全关闭');
+ } else {
+ console.error('❌ 无法获取进程ID');
+ }
+ });
+
+ // 给exec一点时间执行
+ setTimeout(() => {
+ process.exit(0);
+ }, 1000);
+
+ } else {
+ // 前台运行模式
+ const child = spawn('node', [APP_FILE], {
+ stdio: 'inherit'
+ });
+
+ console.log(`🔄 服务已启动 (PID: ${child.pid})`);
+
+ this.writePid(child.pid);
+
+ // 监听进程退出
+ child.on('exit', (code, signal) => {
+ this.removePidFile();
+ if (code !== 0) {
+ console.log(`💥 进程退出 (代码: ${code}, 信号: ${signal})`);
+ }
+ });
+
+ child.on('error', (error) => {
+ console.error('❌ 启动失败:', error.message);
+ this.removePidFile();
+ });
+ }
+
+ return true;
+ }
+
+ stop() {
+ const status = this.getStatus();
+ if (!status.running) {
+ console.log('⚠️ 服务未在运行');
+ this.removePidFile(); // 清理可能存在的过期PID文件
+ return false;
+ }
+
+ console.log(`🛑 停止服务 (PID: ${status.pid})...`);
+
+ try {
+ // 优雅关闭:先发送SIGTERM
+ process.kill(status.pid, 'SIGTERM');
+
+ // 等待进程退出
+ let attempts = 0;
+ const maxAttempts = 30; // 30秒超时
+
+ const checkExit = setInterval(() => {
+ attempts++;
+ if (!this.isProcessRunning(status.pid)) {
+ clearInterval(checkExit);
+ console.log('✅ 服务已停止');
+ this.removePidFile();
+ return;
+ }
+
+ if (attempts >= maxAttempts) {
+ clearInterval(checkExit);
+ console.log('⚠️ 优雅关闭超时,强制终止进程...');
+ try {
+ process.kill(status.pid, 'SIGKILL');
+ console.log('✅ 服务已强制停止');
+ } catch (error) {
+ console.error('❌ 强制停止失败:', error.message);
+ }
+ this.removePidFile();
+ }
+ }, 1000);
+
+ } catch (error) {
+ console.error('❌ 停止服务失败:', error.message);
+ this.removePidFile();
+ return false;
+ }
+
+ return true;
+ }
+
+ restart(daemon = false) {
+ console.log('🔄 重启服务...');
+ const stopResult = this.stop();
+
+ // 等待停止完成
+ setTimeout(() => {
+ this.start(daemon);
+ }, 2000);
+
+ return true;
+ }
+
+ status() {
+ const status = this.getStatus();
+ if (status.running) {
+ console.log(`✅ 服务正在运行 (PID: ${status.pid})`);
+
+ // 显示进程信息
+ exec(`ps -p ${status.pid} -o pid,ppid,pcpu,pmem,etime,cmd --no-headers`, (error, stdout) => {
+ if (!error && stdout.trim()) {
+ console.log('\n📊 进程信息:');
+ console.log('PID\tPPID\tCPU%\tMEM%\tTIME\t\tCOMMAND');
+ console.log(stdout.trim());
+ }
+ });
+ } else {
+ console.log('❌ 服务未运行');
+ }
+ return status.running;
+ }
+
+ logs(lines = 50) {
+ console.log(`📖 最近 ${lines} 行日志:\n`);
+
+ exec(`tail -n ${lines} ${LOG_FILE}`, (error, stdout) => {
+ if (error) {
+ console.error('读取日志失败:', error.message);
+ return;
+ }
+ console.log(stdout);
+ });
+ }
+
+ help() {
+ console.log(`
+🔧 Claude Relay Service 进程管理器
+
+用法: npm run service [options]
+
+重要提示:
+ 如果要传递参数,请在npm run命令中使用 -- 分隔符
+ npm run service -- [options]
+
+命令:
+ start [-d|--daemon] 启动服务 (-d: 后台运行)
+ stop 停止服务
+ restart [-d|--daemon] 重启服务 (-d: 后台运行)
+ status 查看服务状态
+ logs [lines] 查看日志 (默认50行)
+ help 显示帮助信息
+
+命令缩写:
+ s, start 启动服务
+ r, restart 重启服务
+ st, status 查看状态
+ l, log, logs 查看日志
+ halt, stop 停止服务
+ h, help 显示帮助
+
+示例:
+ npm run service start # 前台启动
+ npm run service -- start -d # 后台启动(正确方式)
+ npm run service:start:d # 后台启动(推荐快捷方式)
+ npm run service:daemon # 后台启动(推荐快捷方式)
+ npm run service stop # 停止服务
+ npm run service -- restart -d # 后台重启(正确方式)
+ npm run service:restart:d # 后台重启(推荐快捷方式)
+ npm run service status # 查看状态
+ npm run service logs # 查看日志
+ npm run service -- logs 100 # 查看最近100行日志
+
+推荐的快捷方式(无需 -- 分隔符):
+ npm run service:start:d # 等同于 npm run service -- start -d
+ npm run service:restart:d # 等同于 npm run service -- restart -d
+ npm run service:daemon # 等同于 npm run service -- start -d
+
+直接使用脚本(推荐):
+ node scripts/manage.js start -d # 后台启动
+ node scripts/manage.js restart -d # 后台重启
+ node scripts/manage.js status # 查看状态
+ node scripts/manage.js logs 100 # 查看最近100行日志
+
+文件位置:
+ PID文件: ${PID_FILE}
+ 日志文件: ${LOG_FILE}
+ 错误日志: ${ERROR_LOG_FILE}
+ `);
+ }
+}
+
+// 主程序
+function main() {
+ const manager = new ServiceManager();
+ const args = process.argv.slice(2);
+ const command = args[0];
+ const isDaemon = args.includes('-d') || args.includes('--daemon');
+
+ switch (command) {
+ case 'start':
+ case 's':
+ manager.start(isDaemon);
+ break;
+ case 'stop':
+ case 'halt':
+ manager.stop();
+ break;
+ case 'restart':
+ case 'r':
+ manager.restart(isDaemon);
+ break;
+ case 'status':
+ case 'st':
+ manager.status();
+ break;
+ case 'logs':
+ case 'log':
+ case 'l':
+ const lines = parseInt(args[1]) || 50;
+ manager.logs(lines);
+ break;
+ case 'help':
+ case '--help':
+ case '-h':
+ case 'h':
+ manager.help();
+ break;
+ default:
+ console.log('❌ 未知命令:', command);
+ manager.help();
+ process.exit(1);
+ }
+}
+
+if (require.main === module) {
+ main();
+}
+
+module.exports = ServiceManager;
\ No newline at end of file
diff --git a/scripts/setup.js b/scripts/setup.js
new file mode 100644
index 00000000..c5b9d2a3
--- /dev/null
+++ b/scripts/setup.js
@@ -0,0 +1,107 @@
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const bcrypt = require('bcryptjs');
+const chalk = require('chalk');
+const ora = require('ora');
+
+const config = require('../config/config');
+
+async function setup() {
+ console.log(chalk.blue.bold('\n🚀 Claude Relay Service 初始化设置\n'));
+
+ const spinner = ora('正在进行初始化设置...').start();
+
+ try {
+ // 1. 创建必要目录
+ const directories = [
+ 'logs',
+ 'data',
+ 'temp'
+ ];
+
+ directories.forEach(dir => {
+ const dirPath = path.join(__dirname, '..', dir);
+ if (!fs.existsSync(dirPath)) {
+ fs.mkdirSync(dirPath, { recursive: true });
+ }
+ });
+
+ // 2. 生成环境配置文件
+ if (!fs.existsSync(path.join(__dirname, '..', '.env'))) {
+ const envTemplate = fs.readFileSync(path.join(__dirname, '..', '.env.example'), 'utf8');
+
+ // 生成随机密钥
+ const jwtSecret = crypto.randomBytes(64).toString('hex');
+ const encryptionKey = crypto.randomBytes(32).toString('hex');
+
+ const envContent = envTemplate
+ .replace('your-jwt-secret-here', jwtSecret)
+ .replace('your-encryption-key-here', encryptionKey);
+
+ fs.writeFileSync(path.join(__dirname, '..', '.env'), envContent);
+ }
+
+ // 3. 生成随机管理员凭据
+ const adminUsername = `cr_admin_${crypto.randomBytes(4).toString('hex')}`;
+ const adminPassword = crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
+
+ // 4. 创建初始化完成标记文件
+ const initData = {
+ initializedAt: new Date().toISOString(),
+ adminUsername,
+ adminPassword,
+ version: '1.0.0'
+ };
+
+ fs.writeFileSync(
+ path.join(__dirname, '..', 'data', 'init.json'),
+ JSON.stringify(initData, null, 2)
+ );
+
+ spinner.succeed('初始化设置完成');
+
+ console.log(chalk.green('\n✅ 设置完成!\n'));
+ console.log(chalk.yellow('📋 重要信息:\n'));
+ console.log(` 管理员用户名: ${chalk.cyan(adminUsername)}`);
+ console.log(` 管理员密码: ${chalk.cyan(adminPassword)}`);
+ console.log(chalk.red('\n⚠️ 请立即保存这些凭据!首次登录后建议修改密码。\n'));
+
+ console.log(chalk.blue('🚀 启动服务:\n'));
+ console.log(' npm start - 启动生产服务');
+ console.log(' npm run dev - 启动开发服务');
+ console.log(' npm run cli admin - 管理员CLI工具\n');
+
+ console.log(chalk.blue('🌐 访问地址:\n'));
+ console.log(` Web管理界面: http://localhost:${config.server.port}/web`);
+ console.log(` API端点: http://localhost:${config.server.port}/api/v1/messages`);
+ console.log(` 健康检查: http://localhost:${config.server.port}/health\n`);
+
+ } catch (error) {
+ spinner.fail('初始化设置失败');
+ console.error(chalk.red('❌ 错误:'), error.message);
+ process.exit(1);
+ }
+}
+
+// 检查是否已初始化
+function checkInitialized() {
+ const initFile = path.join(__dirname, '..', 'data', 'init.json');
+ if (fs.existsSync(initFile)) {
+ const initData = JSON.parse(fs.readFileSync(initFile, 'utf8'));
+ console.log(chalk.yellow('⚠️ 服务已经初始化过了!'));
+ console.log(` 初始化时间: ${new Date(initData.initializedAt).toLocaleString()}`);
+ console.log(` 管理员用户名: ${initData.adminUsername}`);
+ console.log('\n如需重新初始化,请删除 data/init.json 文件。');
+ return true;
+ }
+ return false;
+}
+
+if (require.main === module) {
+ if (!checkInitialized()) {
+ setup();
+ }
+}
+
+module.exports = { setup, checkInitialized };
\ No newline at end of file
diff --git a/src/app.js b/src/app.js
new file mode 100644
index 00000000..12b2a412
--- /dev/null
+++ b/src/app.js
@@ -0,0 +1,367 @@
+const express = require('express');
+const cors = require('cors');
+const helmet = require('helmet');
+const morgan = require('morgan');
+const compression = require('compression');
+const path = require('path');
+const fs = require('fs');
+const bcrypt = require('bcryptjs');
+
+const config = require('../config/config');
+const logger = require('./utils/logger');
+const redis = require('./models/redis');
+const pricingService = require('./services/pricingService');
+
+// Import routes
+const apiRoutes = require('./routes/api');
+const adminRoutes = require('./routes/admin');
+const webRoutes = require('./routes/web');
+
+// Import middleware
+const {
+ corsMiddleware,
+ requestLogger,
+ securityMiddleware,
+ errorHandler,
+ globalRateLimit,
+ requestSizeLimit
+} = require('./middleware/auth');
+
+class Application {
+ constructor() {
+ this.app = express();
+ this.server = null;
+ }
+
+ async initialize() {
+ try {
+ // 🔗 连接Redis
+ logger.info('🔄 Connecting to Redis...');
+ await redis.connect();
+ logger.success('✅ Redis connected successfully');
+
+ // 💰 初始化价格服务
+ logger.info('🔄 Initializing pricing service...');
+ await pricingService.initialize();
+
+ // 🔧 初始化管理员凭据
+ logger.info('🔄 Initializing admin credentials...');
+ await this.initializeAdmin();
+
+ // 🛡️ 安全中间件
+ this.app.use(helmet({
+ contentSecurityPolicy: false, // 允许内联样式和脚本
+ crossOriginEmbedderPolicy: false
+ }));
+
+ // 🌐 CORS
+ if (config.web.enableCors) {
+ this.app.use(cors());
+ } else {
+ this.app.use(corsMiddleware);
+ }
+
+ // 📦 压缩
+ this.app.use(compression());
+
+ // 🚦 全局速率限制(仅在生产环境启用)
+ if (process.env.NODE_ENV === 'production') {
+ this.app.use(globalRateLimit);
+ }
+
+ // 📏 请求大小限制
+ this.app.use(requestSizeLimit);
+
+ // 📝 请求日志(使用自定义logger而不是morgan)
+ this.app.use(requestLogger);
+
+ // 🔧 基础中间件
+ this.app.use(express.json({
+ limit: '10mb',
+ verify: (req, res, buf, encoding) => {
+ // 验证JSON格式
+ if (buf && buf.length && !buf.toString(encoding || 'utf8').trim()) {
+ throw new Error('Invalid JSON: empty body');
+ }
+ }
+ }));
+ this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+ this.app.use(securityMiddleware);
+
+ // 🎯 信任代理
+ if (config.server.trustProxy) {
+ this.app.set('trust proxy', 1);
+ }
+
+ // 🛣️ 路由
+ this.app.use('/api', apiRoutes);
+ this.app.use('/admin', adminRoutes);
+ this.app.use('/web', webRoutes);
+
+ // 🏠 根路径重定向到管理界面
+ this.app.get('/', (req, res) => {
+ res.redirect('/web');
+ });
+
+ // 🏥 增强的健康检查端点
+ this.app.get('/health', async (req, res) => {
+ try {
+ const timer = logger.timer('health-check');
+
+ // 检查各个组件健康状态
+ const [redisHealth, loggerHealth] = await Promise.all([
+ this.checkRedisHealth(),
+ this.checkLoggerHealth()
+ ]);
+
+ const memory = process.memoryUsage();
+ const health = {
+ status: 'healthy',
+ service: 'claude-relay-service',
+ version: '1.0.0',
+ timestamp: new Date().toISOString(),
+ uptime: process.uptime(),
+ memory: {
+ used: Math.round(memory.heapUsed / 1024 / 1024) + 'MB',
+ total: Math.round(memory.heapTotal / 1024 / 1024) + 'MB',
+ external: Math.round(memory.external / 1024 / 1024) + 'MB'
+ },
+ components: {
+ redis: redisHealth,
+ logger: loggerHealth
+ },
+ stats: logger.getStats()
+ };
+
+ timer.end('completed');
+ res.json(health);
+ } catch (error) {
+ logger.error('❌ Health check failed:', { error: error.message, stack: error.stack });
+ res.status(503).json({
+ status: 'unhealthy',
+ error: error.message,
+ timestamp: new Date().toISOString()
+ });
+ }
+ });
+
+ // 📊 指标端点
+ this.app.get('/metrics', async (req, res) => {
+ try {
+ const stats = await redis.getSystemStats();
+ const metrics = {
+ ...stats,
+ uptime: process.uptime(),
+ memory: process.memoryUsage(),
+ timestamp: new Date().toISOString()
+ };
+
+ res.json(metrics);
+ } catch (error) {
+ logger.error('❌ Metrics collection failed:', error);
+ res.status(500).json({ error: 'Failed to collect metrics' });
+ }
+ });
+
+ // 🚫 404 处理
+ this.app.use('*', (req, res) => {
+ res.status(404).json({
+ error: 'Not Found',
+ message: `Route ${req.originalUrl} not found`,
+ timestamp: new Date().toISOString()
+ });
+ });
+
+ // 🚨 错误处理
+ this.app.use(errorHandler);
+
+ logger.success('✅ Application initialized successfully');
+
+ } catch (error) {
+ logger.error('💥 Application initialization failed:', error);
+ throw error;
+ }
+ }
+
+ // 🔧 初始化管理员凭据
+ async initializeAdmin() {
+ try {
+ // 检查Redis中是否已存在管理员凭据
+ const existingAdmin = await redis.getSession('admin_credentials');
+
+ if (!existingAdmin || Object.keys(existingAdmin).length === 0) {
+ // 尝试从初始化文件读取
+ const initFilePath = path.join(__dirname, '..', 'data', 'init.json');
+
+ if (fs.existsSync(initFilePath)) {
+ const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8'));
+
+ // 将明文密码哈希化
+ const saltRounds = 10;
+ const passwordHash = await bcrypt.hash(initData.adminPassword, saltRounds);
+
+ // 存储到Redis
+ const adminCredentials = {
+ username: initData.adminUsername,
+ passwordHash: passwordHash,
+ createdAt: new Date().toISOString(),
+ lastLogin: null
+ };
+
+ await redis.setSession('admin_credentials', adminCredentials);
+
+ logger.success('✅ Admin credentials initialized from setup data');
+ } else {
+ logger.warn('⚠️ No admin credentials found. Please run npm run setup first.');
+ }
+ } else {
+ logger.info('ℹ️ Admin credentials already exist in Redis');
+ }
+ } catch (error) {
+ logger.error('❌ Failed to initialize admin credentials:', { error: error.message, stack: error.stack });
+ throw error;
+ }
+ }
+
+ // 🔍 Redis健康检查
+ async checkRedisHealth() {
+ try {
+ const start = Date.now();
+ await redis.getClient().ping();
+ const latency = Date.now() - start;
+
+ return {
+ status: 'healthy',
+ connected: redis.isConnected,
+ latency: `${latency}ms`
+ };
+ } catch (error) {
+ return {
+ status: 'unhealthy',
+ connected: false,
+ error: error.message
+ };
+ }
+ }
+
+ // 📝 Logger健康检查
+ async checkLoggerHealth() {
+ try {
+ const health = logger.healthCheck();
+ return {
+ status: health.healthy ? 'healthy' : 'unhealthy',
+ ...health
+ };
+ } catch (error) {
+ return {
+ status: 'unhealthy',
+ error: error.message
+ };
+ }
+ }
+
+ async start() {
+ try {
+ await this.initialize();
+
+ this.server = this.app.listen(config.server.port, config.server.host, () => {
+ logger.start(`🚀 Claude Relay Service started on ${config.server.host}:${config.server.port}`);
+ logger.info(`🌐 Web interface: http://${config.server.host}:${config.server.port}/web`);
+ logger.info(`🔗 API endpoint: http://${config.server.host}:${config.server.port}/api/v1/messages`);
+ logger.info(`⚙️ Admin API: http://${config.server.host}:${config.server.port}/admin`);
+ logger.info(`🏥 Health check: http://${config.server.host}:${config.server.port}/health`);
+ logger.info(`📊 Metrics: http://${config.server.host}:${config.server.port}/metrics`);
+ });
+
+ // 🔄 定期清理任务
+ this.startCleanupTasks();
+
+ // 🛑 优雅关闭
+ this.setupGracefulShutdown();
+
+ } catch (error) {
+ logger.error('💥 Failed to start server:', error);
+ process.exit(1);
+ }
+ }
+
+ startCleanupTasks() {
+ // 🧹 每小时清理一次过期数据
+ setInterval(async () => {
+ try {
+ logger.info('🧹 Starting scheduled cleanup...');
+
+ const apiKeyService = require('./services/apiKeyService');
+ const claudeAccountService = require('./services/claudeAccountService');
+
+ const [expiredKeys, errorAccounts] = await Promise.all([
+ apiKeyService.cleanupExpiredKeys(),
+ claudeAccountService.cleanupErrorAccounts()
+ ]);
+
+ await redis.cleanup();
+
+ logger.success(`🧹 Cleanup completed: ${expiredKeys} expired keys, ${errorAccounts} error accounts reset`);
+ } catch (error) {
+ logger.error('❌ Cleanup task failed:', error);
+ }
+ }, config.system.cleanupInterval);
+
+ logger.info(`🔄 Cleanup tasks scheduled every ${config.system.cleanupInterval / 1000 / 60} minutes`);
+ }
+
+ setupGracefulShutdown() {
+ const shutdown = async (signal) => {
+ logger.info(`🛑 Received ${signal}, starting graceful shutdown...`);
+
+ if (this.server) {
+ this.server.close(async () => {
+ logger.info('🚪 HTTP server closed');
+
+ try {
+ await redis.disconnect();
+ logger.info('👋 Redis disconnected');
+ } catch (error) {
+ logger.error('❌ Error disconnecting Redis:', error);
+ }
+
+ logger.success('✅ Graceful shutdown completed');
+ process.exit(0);
+ });
+
+ // 强制关闭超时
+ setTimeout(() => {
+ logger.warn('⚠️ Forced shutdown due to timeout');
+ process.exit(1);
+ }, 10000);
+ } else {
+ process.exit(0);
+ }
+ };
+
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
+ process.on('SIGINT', () => shutdown('SIGINT'));
+
+ // 处理未捕获异常
+ process.on('uncaughtException', (error) => {
+ logger.error('💥 Uncaught exception:', error);
+ shutdown('uncaughtException');
+ });
+
+ process.on('unhandledRejection', (reason, promise) => {
+ logger.error('💥 Unhandled rejection at:', promise, 'reason:', reason);
+ shutdown('unhandledRejection');
+ });
+ }
+}
+
+// 启动应用
+if (require.main === module) {
+ const app = new Application();
+ app.start().catch((error) => {
+ logger.error('💥 Application startup failed:', error);
+ process.exit(1);
+ });
+}
+
+module.exports = Application;
\ No newline at end of file
diff --git a/src/middleware/auth.js b/src/middleware/auth.js
new file mode 100644
index 00000000..0fb71e7a
--- /dev/null
+++ b/src/middleware/auth.js
@@ -0,0 +1,532 @@
+const apiKeyService = require('../services/apiKeyService');
+const logger = require('../utils/logger');
+const redis = require('../models/redis');
+const { RateLimiterRedis } = require('rate-limiter-flexible');
+
+// 🔑 API Key验证中间件(优化版)
+const authenticateApiKey = async (req, res, next) => {
+ const startTime = Date.now();
+
+ try {
+ // 安全提取API Key,支持多种格式
+ const apiKey = req.headers['x-api-key'] ||
+ req.headers['authorization']?.replace(/^Bearer\s+/i, '') ||
+ req.headers['api-key'];
+
+ if (!apiKey) {
+ logger.security(`🔒 Missing API key attempt from ${req.ip || 'unknown'}`);
+ return res.status(401).json({
+ error: 'Missing API key',
+ message: 'Please provide an API key in the x-api-key header or Authorization header'
+ });
+ }
+
+ // 基本API Key格式验证
+ if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) {
+ logger.security(`🔒 Invalid API key format from ${req.ip || 'unknown'}`);
+ return res.status(401).json({
+ error: 'Invalid API key format',
+ message: 'API key format is invalid'
+ });
+ }
+
+ // 验证API Key(带缓存优化)
+ const validation = await apiKeyService.validateApiKey(apiKey);
+
+ if (!validation.valid) {
+ const clientIP = req.ip || req.connection?.remoteAddress || 'unknown';
+ logger.security(`🔒 Invalid API key attempt: ${validation.error} from ${clientIP}`);
+ return res.status(401).json({
+ error: 'Invalid API key',
+ message: validation.error
+ });
+ }
+
+ // 检查速率限制(优化:只在验证成功后检查)
+ const rateLimitResult = await apiKeyService.checkRateLimit(validation.keyData.id);
+
+ if (!rateLimitResult.allowed) {
+ logger.security(`🚦 Rate limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name})`);
+ return res.status(429).json({
+ error: 'Rate limit exceeded',
+ message: `Too many requests. Limit: ${rateLimitResult.limit} requests per minute`,
+ resetTime: rateLimitResult.resetTime,
+ retryAfter: rateLimitResult.resetTime
+ });
+ }
+
+ // 设置标准速率限制响应头
+ res.setHeader('X-RateLimit-Limit', rateLimitResult.limit);
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, rateLimitResult.limit - rateLimitResult.current));
+ res.setHeader('X-RateLimit-Reset', rateLimitResult.resetTime);
+ res.setHeader('X-RateLimit-Policy', `${rateLimitResult.limit};w=60`);
+
+ // 将验证信息添加到请求对象(只包含必要信息)
+ req.apiKey = {
+ id: validation.keyData.id,
+ name: validation.keyData.name,
+ tokenLimit: validation.keyData.tokenLimit,
+ requestLimit: validation.keyData.requestLimit,
+ claudeAccountId: validation.keyData.claudeAccountId
+ };
+ req.usage = validation.keyData.usage;
+
+ const authDuration = Date.now() - startTime;
+ logger.api(`🔓 Authenticated request from key: ${validation.keyData.name} (${validation.keyData.id}) in ${authDuration}ms`);
+
+ next();
+ } catch (error) {
+ const authDuration = Date.now() - startTime;
+ logger.error(`❌ Authentication middleware error (${authDuration}ms):`, {
+ error: error.message,
+ stack: error.stack,
+ ip: req.ip,
+ userAgent: req.get('User-Agent'),
+ url: req.originalUrl
+ });
+
+ res.status(500).json({
+ error: 'Authentication error',
+ message: 'Internal server error during authentication'
+ });
+ }
+};
+
+// 🛡️ 管理员验证中间件(优化版)
+const authenticateAdmin = async (req, res, next) => {
+ const startTime = Date.now();
+
+ try {
+ // 安全提取token,支持多种方式
+ const token = req.headers['authorization']?.replace(/^Bearer\s+/i, '') ||
+ req.cookies?.adminToken ||
+ req.headers['x-admin-token'];
+
+ if (!token) {
+ logger.security(`🔒 Missing admin token attempt from ${req.ip || 'unknown'}`);
+ return res.status(401).json({
+ error: 'Missing admin token',
+ message: 'Please provide an admin token'
+ });
+ }
+
+ // 基本token格式验证
+ if (typeof token !== 'string' || token.length < 32 || token.length > 512) {
+ logger.security(`🔒 Invalid admin token format from ${req.ip || 'unknown'}`);
+ return res.status(401).json({
+ error: 'Invalid admin token format',
+ message: 'Admin token format is invalid'
+ });
+ }
+
+ // 获取管理员会话(带超时处理)
+ const adminSession = await Promise.race([
+ redis.getSession(token),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Session lookup timeout')), 5000)
+ )
+ ]);
+
+ if (!adminSession || Object.keys(adminSession).length === 0) {
+ logger.security(`🔒 Invalid admin token attempt from ${req.ip || 'unknown'}`);
+ return res.status(401).json({
+ error: 'Invalid admin token',
+ message: 'Invalid or expired admin session'
+ });
+ }
+
+ // 检查会话活跃性(可选:检查最后活动时间)
+ const now = new Date();
+ const lastActivity = new Date(adminSession.lastActivity || adminSession.loginTime);
+ const inactiveDuration = now - lastActivity;
+ const maxInactivity = 24 * 60 * 60 * 1000; // 24小时
+
+ if (inactiveDuration > maxInactivity) {
+ logger.security(`🔒 Expired admin session for ${adminSession.username} from ${req.ip || 'unknown'}`);
+ await redis.deleteSession(token); // 清理过期会话
+ return res.status(401).json({
+ error: 'Session expired',
+ message: 'Admin session has expired due to inactivity'
+ });
+ }
+
+ // 更新最后活动时间(异步,不阻塞请求)
+ redis.setSession(token, {
+ ...adminSession,
+ lastActivity: now.toISOString()
+ }, 86400).catch(error => {
+ logger.error('Failed to update admin session activity:', error);
+ });
+
+ // 设置管理员信息(只包含必要信息)
+ req.admin = {
+ id: adminSession.adminId || 'admin',
+ username: adminSession.username,
+ sessionId: token,
+ loginTime: adminSession.loginTime
+ };
+
+ const authDuration = Date.now() - startTime;
+ logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`);
+
+ next();
+ } catch (error) {
+ const authDuration = Date.now() - startTime;
+ logger.error(`❌ Admin authentication error (${authDuration}ms):`, {
+ error: error.message,
+ ip: req.ip,
+ userAgent: req.get('User-Agent'),
+ url: req.originalUrl
+ });
+
+ res.status(500).json({
+ error: 'Authentication error',
+ message: 'Internal server error during admin authentication'
+ });
+ }
+};
+
+// 注意:使用统计现在直接在/api/v1/messages路由中处理,
+// 以便从Claude API响应中提取真实的usage数据
+
+// 🚦 CORS中间件(优化版)
+const corsMiddleware = (req, res, next) => {
+ const origin = req.headers.origin;
+
+ // 允许的源(可以从配置文件读取)
+ const allowedOrigins = [
+ 'http://localhost:3000',
+ 'https://localhost:3000',
+ 'http://127.0.0.1:3000',
+ 'https://127.0.0.1:3000'
+ ];
+
+ // 设置CORS头
+ if (allowedOrigins.includes(origin) || !origin) {
+ res.header('Access-Control-Allow-Origin', origin || '*');
+ }
+
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+ res.header('Access-Control-Allow-Headers', [
+ 'Origin',
+ 'X-Requested-With',
+ 'Content-Type',
+ 'Accept',
+ 'Authorization',
+ 'x-api-key',
+ 'api-key',
+ 'x-admin-token'
+ ].join(', '));
+
+ res.header('Access-Control-Expose-Headers', [
+ 'X-RateLimit-Limit',
+ 'X-RateLimit-Remaining',
+ 'X-RateLimit-Reset',
+ 'X-RateLimit-Policy'
+ ].join(', '));
+
+ res.header('Access-Control-Max-Age', '86400'); // 24小时预检缓存
+ res.header('Access-Control-Allow-Credentials', 'true');
+
+ if (req.method === 'OPTIONS') {
+ res.status(204).end();
+ } else {
+ next();
+ }
+};
+
+// 📝 请求日志中间件(优化版)
+const requestLogger = (req, res, next) => {
+ const start = Date.now();
+ const requestId = Math.random().toString(36).substring(2, 15);
+
+ // 添加请求ID到请求对象
+ req.requestId = requestId;
+ res.setHeader('X-Request-ID', requestId);
+
+ // 获取客户端信息
+ const clientIP = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown';
+ const userAgent = req.get('User-Agent') || 'unknown';
+ const referer = req.get('Referer') || 'none';
+
+ // 记录请求开始
+ if (req.originalUrl !== '/health') { // 避免健康检查日志过多
+ logger.request(`▶️ [${requestId}] ${req.method} ${req.originalUrl} | IP: ${clientIP}`);
+ }
+
+ res.on('finish', () => {
+ const duration = Date.now() - start;
+ const contentLength = res.get('Content-Length') || '0';
+
+ // 构建日志元数据
+ const logMetadata = {
+ requestId,
+ method: req.method,
+ url: req.originalUrl,
+ status: res.statusCode,
+ duration,
+ contentLength,
+ ip: clientIP,
+ userAgent,
+ referer
+ };
+
+ // 根据状态码选择日志级别
+ if (res.statusCode >= 500) {
+ logger.error(`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`, logMetadata);
+ } else if (res.statusCode >= 400) {
+ logger.warn(`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`, logMetadata);
+ } else if (req.originalUrl !== '/health') {
+ logger.request(`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`, logMetadata);
+ }
+
+ // API Key相关日志
+ if (req.apiKey) {
+ logger.api(`📱 [${requestId}] Request from ${req.apiKey.name} (${req.apiKey.id}) | ${duration}ms`);
+ }
+
+ // 慢请求警告
+ if (duration > 5000) {
+ logger.warn(`🐌 [${requestId}] Slow request detected: ${duration}ms for ${req.method} ${req.originalUrl}`);
+ }
+ });
+
+ res.on('error', (error) => {
+ const duration = Date.now() - start;
+ logger.error(`💥 [${requestId}] Response error after ${duration}ms:`, error);
+ });
+
+ next();
+};
+
+// 🛡️ 安全中间件(增强版)
+const securityMiddleware = (req, res, next) => {
+ // 设置基础安全头
+ res.setHeader('X-Content-Type-Options', 'nosniff');
+ res.setHeader('X-Frame-Options', 'DENY');
+ res.setHeader('X-XSS-Protection', '1; mode=block');
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
+
+ // 添加更多安全头
+ res.setHeader('X-DNS-Prefetch-Control', 'off');
+ res.setHeader('X-Download-Options', 'noopen');
+ res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
+
+ // Cross-Origin-Opener-Policy (仅对可信来源设置)
+ const host = req.get('host') || '';
+ const isLocalhost = host.includes('localhost') || host.includes('127.0.0.1') || host.includes('0.0.0.0');
+ const isHttps = req.secure || req.headers['x-forwarded-proto'] === 'https';
+
+ if (isLocalhost || isHttps) {
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
+ res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
+ res.setHeader('Origin-Agent-Cluster', '?1');
+ }
+
+ // Content Security Policy (适用于web界面)
+ if (req.path.startsWith('/web') || req.path === '/') {
+ res.setHeader('Content-Security-Policy', [
+ 'default-src \'self\'',
+ 'script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' https://unpkg.com https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net',
+ 'style-src \'self\' \'unsafe-inline\' https://cdn.tailwindcss.com https://cdnjs.cloudflare.com',
+ 'font-src \'self\' https://cdnjs.cloudflare.com',
+ 'img-src \'self\' data:',
+ 'connect-src \'self\'',
+ 'frame-ancestors \'none\'',
+ 'base-uri \'self\'',
+ 'form-action \'self\''
+ ].join('; '));
+ }
+
+ // Strict Transport Security (HTTPS)
+ if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
+ res.setHeader('Strict-Transport-Security', 'max-age=15552000; includeSubDomains');
+ }
+
+ // 移除泄露服务器信息的头
+ res.removeHeader('X-Powered-By');
+ res.removeHeader('Server');
+
+ // 防止信息泄露
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+
+ next();
+};
+
+// 🚨 错误处理中间件(增强版)
+const errorHandler = (error, req, res, _next) => {
+ const requestId = req.requestId || 'unknown';
+ const isDevelopment = process.env.NODE_ENV === 'development';
+
+ // 记录详细错误信息
+ logger.error(`💥 [${requestId}] Unhandled error:`, {
+ error: error.message,
+ stack: error.stack,
+ url: req.originalUrl,
+ method: req.method,
+ ip: req.ip || 'unknown',
+ userAgent: req.get('User-Agent') || 'unknown',
+ apiKey: req.apiKey ? req.apiKey.id : 'none',
+ admin: req.admin ? req.admin.username : 'none'
+ });
+
+ // 确定HTTP状态码
+ let statusCode = 500;
+ let errorMessage = 'Internal Server Error';
+ let userMessage = 'Something went wrong';
+
+ if (error.status && error.status >= 400 && error.status < 600) {
+ statusCode = error.status;
+ }
+
+ // 根据错误类型提供友好的错误消息
+ switch (error.name) {
+ case 'ValidationError':
+ statusCode = 400;
+ errorMessage = 'Validation Error';
+ userMessage = 'Invalid input data';
+ break;
+ case 'CastError':
+ statusCode = 400;
+ errorMessage = 'Cast Error';
+ userMessage = 'Invalid data format';
+ break;
+ case 'MongoError':
+ case 'RedisError':
+ statusCode = 503;
+ errorMessage = 'Database Error';
+ userMessage = 'Database temporarily unavailable';
+ break;
+ case 'TimeoutError':
+ statusCode = 408;
+ errorMessage = 'Request Timeout';
+ userMessage = 'Request took too long to process';
+ break;
+ default:
+ if (error.message && !isDevelopment) {
+ // 在生产环境中,只显示安全的错误消息
+ if (error.message.includes('ECONNREFUSED')) {
+ userMessage = 'Service temporarily unavailable';
+ } else if (error.message.includes('timeout')) {
+ userMessage = 'Request timeout';
+ }
+ }
+ }
+
+ // 设置响应头
+ res.setHeader('X-Request-ID', requestId);
+
+ // 构建错误响应
+ const errorResponse = {
+ error: errorMessage,
+ message: isDevelopment ? error.message : userMessage,
+ requestId,
+ timestamp: new Date().toISOString()
+ };
+
+ // 在开发环境中包含更多调试信息
+ if (isDevelopment) {
+ errorResponse.stack = error.stack;
+ errorResponse.url = req.originalUrl;
+ errorResponse.method = req.method;
+ }
+
+ res.status(statusCode).json(errorResponse);
+};
+
+// 🌐 全局速率限制中间件(延迟初始化)
+let rateLimiter = null;
+
+const getRateLimiter = () => {
+ if (!rateLimiter) {
+ try {
+ const client = redis.getClient();
+ if (!client) {
+ logger.warn('⚠️ Redis client not available for rate limiter');
+ return null;
+ }
+
+ rateLimiter = new RateLimiterRedis({
+ storeClient: client,
+ keyPrefix: 'global_rate_limit',
+ points: 1000, // 请求数量
+ duration: 900, // 15分钟 (900秒)
+ blockDuration: 900, // 阻塞时间15分钟
+ });
+
+ logger.info('✅ Rate limiter initialized successfully');
+ } catch (error) {
+ logger.warn('⚠️ Rate limiter initialization failed, using fallback', { error: error.message });
+ return null;
+ }
+ }
+ return rateLimiter;
+};
+
+const globalRateLimit = async (req, res, next) => {
+ // 跳过健康检查和内部请求
+ if (req.path === '/health' || req.path === '/api/health') {
+ return next();
+ }
+
+ const limiter = getRateLimiter();
+ if (!limiter) {
+ // 如果Redis不可用,直接跳过速率限制
+ return next();
+ }
+
+ const clientIP = req.ip || req.connection?.remoteAddress || 'unknown';
+
+ try {
+ await limiter.consume(clientIP);
+ next();
+ } catch (rejRes) {
+ const remainingPoints = rejRes.remainingPoints || 0;
+ const msBeforeNext = rejRes.msBeforeNext || 900000;
+
+ logger.security(`🚦 Global rate limit exceeded for IP: ${clientIP}`);
+
+ res.set({
+ 'Retry-After': Math.round(msBeforeNext / 1000) || 900,
+ 'X-RateLimit-Limit': 1000,
+ 'X-RateLimit-Remaining': remainingPoints,
+ 'X-RateLimit-Reset': new Date(Date.now() + msBeforeNext).toISOString()
+ });
+
+ res.status(429).json({
+ error: 'Too Many Requests',
+ message: 'Too many requests from this IP, please try again later.',
+ retryAfter: Math.round(msBeforeNext / 1000)
+ });
+ }
+};
+
+// 📊 请求大小限制中间件
+const requestSizeLimit = (req, res, next) => {
+ const maxSize = 10 * 1024 * 1024; // 10MB
+ const contentLength = parseInt(req.headers['content-length'] || '0');
+
+ if (contentLength > maxSize) {
+ logger.security(`🚨 Request too large: ${contentLength} bytes from ${req.ip}`);
+ return res.status(413).json({
+ error: 'Payload Too Large',
+ message: 'Request body size exceeds limit',
+ limit: '10MB'
+ });
+ }
+
+ next();
+};
+
+module.exports = {
+ authenticateApiKey,
+ authenticateAdmin,
+ corsMiddleware,
+ requestLogger,
+ securityMiddleware,
+ errorHandler,
+ globalRateLimit,
+ requestSizeLimit
+};
\ No newline at end of file
diff --git a/src/models/redis.js b/src/models/redis.js
new file mode 100644
index 00000000..33edf935
--- /dev/null
+++ b/src/models/redis.js
@@ -0,0 +1,678 @@
+const Redis = require('ioredis');
+const config = require('../../config/config');
+const logger = require('../utils/logger');
+
+class RedisClient {
+ constructor() {
+ this.client = null;
+ this.isConnected = false;
+ }
+
+ async connect() {
+ try {
+ this.client = new Redis({
+ host: config.redis.host,
+ port: config.redis.port,
+ password: config.redis.password,
+ db: config.redis.db,
+ retryDelayOnFailover: config.redis.retryDelayOnFailover,
+ maxRetriesPerRequest: config.redis.maxRetriesPerRequest,
+ lazyConnect: config.redis.lazyConnect
+ });
+
+ this.client.on('connect', () => {
+ this.isConnected = true;
+ logger.info('🔗 Redis connected successfully');
+ });
+
+ this.client.on('error', (err) => {
+ this.isConnected = false;
+ logger.error('❌ Redis connection error:', err);
+ });
+
+ this.client.on('close', () => {
+ this.isConnected = false;
+ logger.warn('⚠️ Redis connection closed');
+ });
+
+ await this.client.connect();
+ return this.client;
+ } catch (error) {
+ logger.error('💥 Failed to connect to Redis:', error);
+ throw error;
+ }
+ }
+
+ async disconnect() {
+ if (this.client) {
+ await this.client.quit();
+ this.isConnected = false;
+ logger.info('👋 Redis disconnected');
+ }
+ }
+
+ getClient() {
+ if (!this.client || !this.isConnected) {
+ logger.warn('⚠️ Redis client is not connected');
+ return null;
+ }
+ return this.client;
+ }
+
+ // 安全获取客户端(用于关键操作)
+ getClientSafe() {
+ if (!this.client || !this.isConnected) {
+ throw new Error('Redis client is not connected');
+ }
+ return this.client;
+ }
+
+ // 🔑 API Key 相关操作
+ async setApiKey(keyId, keyData, hashedKey = null) {
+ const key = `apikey:${keyId}`;
+ const client = this.getClientSafe();
+
+ // 维护哈希映射表(用于快速查找)
+ // hashedKey参数是实际的哈希值,用于建立映射
+ if (hashedKey) {
+ await client.hset('apikey:hash_map', hashedKey, keyId);
+ }
+
+ await client.hset(key, keyData);
+ await client.expire(key, 86400 * 365); // 1年过期
+ }
+
+ async getApiKey(keyId) {
+ const key = `apikey:${keyId}`;
+ return await this.client.hgetall(key);
+ }
+
+ async deleteApiKey(keyId) {
+ const key = `apikey:${keyId}`;
+
+ // 获取要删除的API Key哈希值,以便从映射表中移除
+ const keyData = await this.client.hgetall(key);
+ if (keyData && keyData.apiKey) {
+ // keyData.apiKey现在存储的是哈希值,直接从映射表删除
+ await this.client.hdel('apikey:hash_map', keyData.apiKey);
+ }
+
+ return await this.client.del(key);
+ }
+
+ async getAllApiKeys() {
+ const keys = await this.client.keys('apikey:*');
+ const apiKeys = [];
+ for (const key of keys) {
+ // 过滤掉hash_map,它不是真正的API Key
+ if (key === 'apikey:hash_map') {
+ continue;
+ }
+
+ const keyData = await this.client.hgetall(key);
+ if (keyData && Object.keys(keyData).length > 0) {
+ apiKeys.push({ id: key.replace('apikey:', ''), ...keyData });
+ }
+ }
+ return apiKeys;
+ }
+
+ // 🔍 通过哈希值查找API Key(性能优化)
+ async findApiKeyByHash(hashedKey) {
+ // 使用反向映射表:hash -> keyId
+ const keyId = await this.client.hget('apikey:hash_map', hashedKey);
+ if (!keyId) {
+ return null;
+ }
+
+ const keyData = await this.client.hgetall(`apikey:${keyId}`);
+ if (keyData && Object.keys(keyData).length > 0) {
+ return { id: keyId, ...keyData };
+ }
+
+ // 如果数据不存在,清理映射表
+ await this.client.hdel('apikey:hash_map', hashedKey);
+ return null;
+ }
+
+ // 📊 使用统计相关操作(支持缓存token统计和模型信息)
+ async incrementTokenUsage(keyId, tokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
+ const key = `usage:${keyId}`;
+ const today = new Date().toISOString().split('T')[0];
+ const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
+ const daily = `usage:daily:${keyId}:${today}`;
+ const monthly = `usage:monthly:${keyId}:${currentMonth}`;
+
+ // 按模型统计的键
+ const modelDaily = `usage:model:daily:${model}:${today}`;
+ const modelMonthly = `usage:model:monthly:${model}:${currentMonth}`;
+
+ // API Key级别的模型统计
+ const keyModelDaily = `usage:${keyId}:model:daily:${model}:${today}`;
+ const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
+
+ // 智能处理输入输出token分配
+ const finalInputTokens = inputTokens || 0;
+ const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
+ const finalCacheCreateTokens = cacheCreateTokens || 0;
+ const finalCacheReadTokens = cacheReadTokens || 0;
+
+ // 重新计算真实的总token数(包括缓存token)
+ const totalTokens = finalInputTokens + finalOutputTokens + finalCacheCreateTokens + finalCacheReadTokens;
+ // 核心token(不包括缓存)- 用于与历史数据兼容
+ const coreTokens = finalInputTokens + finalOutputTokens;
+
+ await Promise.all([
+ // 核心token统计(保持向后兼容)
+ this.client.hincrby(key, 'totalTokens', coreTokens),
+ this.client.hincrby(key, 'totalInputTokens', finalInputTokens),
+ this.client.hincrby(key, 'totalOutputTokens', finalOutputTokens),
+ // 缓存token统计(新增)
+ this.client.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(key, 'totalAllTokens', totalTokens), // 包含所有类型的总token
+ // 请求计数
+ this.client.hincrby(key, 'totalRequests', 1),
+ // 每日统计
+ this.client.hincrby(daily, 'tokens', coreTokens),
+ this.client.hincrby(daily, 'inputTokens', finalInputTokens),
+ this.client.hincrby(daily, 'outputTokens', finalOutputTokens),
+ this.client.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(daily, 'allTokens', totalTokens),
+ this.client.hincrby(daily, 'requests', 1),
+ // 每月统计
+ this.client.hincrby(monthly, 'tokens', coreTokens),
+ this.client.hincrby(monthly, 'inputTokens', finalInputTokens),
+ this.client.hincrby(monthly, 'outputTokens', finalOutputTokens),
+ this.client.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(monthly, 'allTokens', totalTokens),
+ this.client.hincrby(monthly, 'requests', 1),
+ // 按模型统计 - 每日
+ this.client.hincrby(modelDaily, 'inputTokens', finalInputTokens),
+ this.client.hincrby(modelDaily, 'outputTokens', finalOutputTokens),
+ this.client.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(modelDaily, 'allTokens', totalTokens),
+ this.client.hincrby(modelDaily, 'requests', 1),
+ // 按模型统计 - 每月
+ this.client.hincrby(modelMonthly, 'inputTokens', finalInputTokens),
+ this.client.hincrby(modelMonthly, 'outputTokens', finalOutputTokens),
+ this.client.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(modelMonthly, 'allTokens', totalTokens),
+ this.client.hincrby(modelMonthly, 'requests', 1),
+ // API Key级别的模型统计 - 每日
+ this.client.hincrby(keyModelDaily, 'inputTokens', finalInputTokens),
+ this.client.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens),
+ this.client.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(keyModelDaily, 'allTokens', totalTokens),
+ this.client.hincrby(keyModelDaily, 'requests', 1),
+ // API Key级别的模型统计 - 每月
+ this.client.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens),
+ this.client.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens),
+ this.client.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
+ this.client.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens),
+ this.client.hincrby(keyModelMonthly, 'allTokens', totalTokens),
+ this.client.hincrby(keyModelMonthly, 'requests', 1),
+ // 设置过期时间
+ this.client.expire(daily, 86400 * 32), // 32天过期
+ this.client.expire(monthly, 86400 * 365), // 1年过期
+ this.client.expire(modelDaily, 86400 * 32), // 模型每日统计32天过期
+ this.client.expire(modelMonthly, 86400 * 365), // 模型每月统计1年过期
+ this.client.expire(keyModelDaily, 86400 * 32), // API Key模型每日统计32天过期
+ this.client.expire(keyModelMonthly, 86400 * 365) // API Key模型每月统计1年过期
+ ]);
+ }
+
+ async getUsageStats(keyId) {
+ const totalKey = `usage:${keyId}`;
+ const today = new Date().toISOString().split('T')[0];
+ const dailyKey = `usage:daily:${keyId}:${today}`;
+ const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
+ const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`;
+
+ const [total, daily, monthly] = await Promise.all([
+ this.client.hgetall(totalKey),
+ this.client.hgetall(dailyKey),
+ this.client.hgetall(monthlyKey)
+ ]);
+
+ // 获取API Key的创建时间来计算平均值
+ const keyData = await this.client.hgetall(`apikey:${keyId}`);
+ const createdAt = keyData.createdAt ? new Date(keyData.createdAt) : new Date();
+ const now = new Date();
+ const daysSinceCreated = Math.max(1, Math.ceil((now - createdAt) / (1000 * 60 * 60 * 24)));
+
+ const totalTokens = parseInt(total.totalTokens) || 0;
+ const totalRequests = parseInt(total.totalRequests) || 0;
+
+ // 计算平均RPM (requests per minute) 和 TPM (tokens per minute)
+ const totalMinutes = Math.max(1, daysSinceCreated * 24 * 60);
+ const avgRPM = totalRequests / totalMinutes;
+ const avgTPM = totalTokens / totalMinutes;
+
+ // 处理旧数据兼容性(支持缓存token)
+ const handleLegacyData = (data) => {
+ // 优先使用total*字段(存储时使用的字段)
+ const tokens = parseInt(data.totalTokens) || parseInt(data.tokens) || 0;
+ const inputTokens = parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0;
+ const outputTokens = parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0;
+ const requests = parseInt(data.totalRequests) || parseInt(data.requests) || 0;
+
+ // 新增缓存token字段
+ const cacheCreateTokens = parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0;
+ const cacheReadTokens = parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0;
+ const allTokens = parseInt(data.totalAllTokens) || parseInt(data.allTokens) || 0;
+
+ const totalFromSeparate = inputTokens + outputTokens;
+
+ if (totalFromSeparate === 0 && tokens > 0) {
+ // 旧数据:没有输入输出分离
+ return {
+ tokens,
+ inputTokens: Math.round(tokens * 0.3), // 假设30%为输入
+ outputTokens: Math.round(tokens * 0.7), // 假设70%为输出
+ cacheCreateTokens: 0, // 旧数据没有缓存token
+ cacheReadTokens: 0,
+ allTokens: tokens, // 对于旧数据,allTokens等于tokens
+ requests
+ };
+ } else {
+ // 新数据或无数据
+ return {
+ tokens,
+ inputTokens,
+ outputTokens,
+ cacheCreateTokens,
+ cacheReadTokens,
+ allTokens: allTokens || (inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens), // 计算或使用存储的值
+ requests
+ };
+ }
+ };
+
+ const totalData = handleLegacyData(total);
+ const dailyData = handleLegacyData(daily);
+ const monthlyData = handleLegacyData(monthly);
+
+ return {
+ total: totalData,
+ daily: dailyData,
+ monthly: monthlyData,
+ averages: {
+ rpm: Math.round(avgRPM * 100) / 100, // 保留2位小数
+ tpm: Math.round(avgTPM * 100) / 100,
+ dailyRequests: Math.round((totalRequests / daysSinceCreated) * 100) / 100,
+ dailyTokens: Math.round((totalTokens / daysSinceCreated) * 100) / 100
+ }
+ };
+ }
+
+ // 🧹 清空所有API Key的使用统计数据
+ async resetAllUsageStats() {
+ const client = this.getClientSafe();
+ const stats = {
+ deletedKeys: 0,
+ deletedDailyKeys: 0,
+ deletedMonthlyKeys: 0,
+ resetApiKeys: 0
+ };
+
+ try {
+ // 获取所有API Key ID
+ const apiKeyIds = [];
+ const apiKeyKeys = await client.keys('apikey:*');
+
+ for (const key of apiKeyKeys) {
+ if (key === 'apikey:hash_map') continue; // 跳过哈希映射表
+ const keyId = key.replace('apikey:', '');
+ apiKeyIds.push(keyId);
+ }
+
+ // 清空每个API Key的使用统计
+ for (const keyId of apiKeyIds) {
+ // 删除总体使用统计
+ const usageKey = `usage:${keyId}`;
+ const deleted = await client.del(usageKey);
+ if (deleted > 0) {
+ stats.deletedKeys++;
+ }
+
+ // 删除该API Key的每日统计(使用精确的keyId匹配)
+ const dailyKeys = await client.keys(`usage:daily:${keyId}:*`);
+ if (dailyKeys.length > 0) {
+ await client.del(...dailyKeys);
+ stats.deletedDailyKeys += dailyKeys.length;
+ }
+
+ // 删除该API Key的每月统计(使用精确的keyId匹配)
+ const monthlyKeys = await client.keys(`usage:monthly:${keyId}:*`);
+ if (monthlyKeys.length > 0) {
+ await client.del(...monthlyKeys);
+ stats.deletedMonthlyKeys += monthlyKeys.length;
+ }
+
+ // 重置API Key的lastUsedAt字段
+ const keyData = await client.hgetall(`apikey:${keyId}`);
+ if (keyData && Object.keys(keyData).length > 0) {
+ keyData.lastUsedAt = '';
+ await client.hset(`apikey:${keyId}`, keyData);
+ stats.resetApiKeys++;
+ }
+ }
+
+ // 额外清理:删除所有可能遗漏的usage相关键
+ const allUsageKeys = await client.keys('usage:*');
+ if (allUsageKeys.length > 0) {
+ await client.del(...allUsageKeys);
+ stats.deletedKeys += allUsageKeys.length;
+ }
+
+ return stats;
+ } catch (error) {
+ throw new Error(`Failed to reset usage stats: ${error.message}`);
+ }
+ }
+
+ // 🏢 Claude 账户管理
+ async setClaudeAccount(accountId, accountData) {
+ const key = `claude:account:${accountId}`;
+ await this.client.hset(key, accountData);
+ }
+
+ async getClaudeAccount(accountId) {
+ const key = `claude:account:${accountId}`;
+ return await this.client.hgetall(key);
+ }
+
+ async getAllClaudeAccounts() {
+ const keys = await this.client.keys('claude:account:*');
+ const accounts = [];
+ for (const key of keys) {
+ const accountData = await this.client.hgetall(key);
+ if (accountData && Object.keys(accountData).length > 0) {
+ accounts.push({ id: key.replace('claude:account:', ''), ...accountData });
+ }
+ }
+ return accounts;
+ }
+
+ async deleteClaudeAccount(accountId) {
+ const key = `claude:account:${accountId}`;
+ return await this.client.del(key);
+ }
+
+ // 🔐 会话管理
+ async setSession(sessionId, sessionData, ttl = 86400) {
+ const key = `session:${sessionId}`;
+ await this.client.hset(key, sessionData);
+ await this.client.expire(key, ttl);
+ }
+
+ async getSession(sessionId) {
+ const key = `session:${sessionId}`;
+ return await this.client.hgetall(key);
+ }
+
+ async deleteSession(sessionId) {
+ const key = `session:${sessionId}`;
+ return await this.client.del(key);
+ }
+
+ // 🗝️ API Key哈希索引管理
+ async setApiKeyHash(hashedKey, keyData, ttl = 0) {
+ const key = `apikey_hash:${hashedKey}`;
+ await this.client.hset(key, keyData);
+ if (ttl > 0) {
+ await this.client.expire(key, ttl);
+ }
+ }
+
+ async getApiKeyHash(hashedKey) {
+ const key = `apikey_hash:${hashedKey}`;
+ return await this.client.hgetall(key);
+ }
+
+ async deleteApiKeyHash(hashedKey) {
+ const key = `apikey_hash:${hashedKey}`;
+ return await this.client.del(key);
+ }
+
+ // 🔗 OAuth会话管理
+ async setOAuthSession(sessionId, sessionData, ttl = 600) { // 10分钟过期
+ const key = `oauth:${sessionId}`;
+ await this.client.hset(key, sessionData);
+ await this.client.expire(key, ttl);
+ }
+
+ async getOAuthSession(sessionId) {
+ const key = `oauth:${sessionId}`;
+ return await this.client.hgetall(key);
+ }
+
+ async deleteOAuthSession(sessionId) {
+ const key = `oauth:${sessionId}`;
+ return await this.client.del(key);
+ }
+
+ // 🚦 速率限制
+ async checkRateLimit(identifier, limit = 100, window = 60) {
+ const key = `ratelimit:${identifier}`;
+ const current = await this.client.incr(key);
+
+ if (current === 1) {
+ await this.client.expire(key, window);
+ }
+
+ return {
+ allowed: current <= limit,
+ current,
+ limit,
+ resetTime: await this.client.ttl(key)
+ };
+ }
+
+ // 📈 系统统计
+ async getSystemStats() {
+ const keys = await Promise.all([
+ this.client.keys('apikey:*'),
+ this.client.keys('claude:account:*'),
+ this.client.keys('usage:*')
+ ]);
+
+ return {
+ totalApiKeys: keys[0].length,
+ totalClaudeAccounts: keys[1].length,
+ totalUsageRecords: keys[2].length
+ };
+ }
+
+ // 📊 获取今日系统统计
+ async getTodayStats() {
+ try {
+ const today = new Date().toISOString().split('T')[0];
+ const dailyKeys = await this.client.keys(`usage:daily:*:${today}`);
+
+ let totalRequestsToday = 0;
+ let totalTokensToday = 0;
+ let totalInputTokensToday = 0;
+ let totalOutputTokensToday = 0;
+ let totalCacheCreateTokensToday = 0;
+ let totalCacheReadTokensToday = 0;
+
+ // 批量获取所有今日数据,提高性能
+ if (dailyKeys.length > 0) {
+ const pipeline = this.client.pipeline();
+ dailyKeys.forEach(key => pipeline.hgetall(key));
+ const results = await pipeline.exec();
+
+ for (const [error, dailyData] of results) {
+ if (error || !dailyData) continue;
+
+ totalRequestsToday += parseInt(dailyData.requests) || 0;
+ const currentDayTokens = parseInt(dailyData.tokens) || 0;
+ totalTokensToday += currentDayTokens;
+
+ // 处理旧数据兼容性:如果有总token但没有输入输出分离,则使用总token作为输出token
+ const inputTokens = parseInt(dailyData.inputTokens) || 0;
+ const outputTokens = parseInt(dailyData.outputTokens) || 0;
+ const cacheCreateTokens = parseInt(dailyData.cacheCreateTokens) || 0;
+ const cacheReadTokens = parseInt(dailyData.cacheReadTokens) || 0;
+ const totalTokensFromSeparate = inputTokens + outputTokens;
+
+ if (totalTokensFromSeparate === 0 && currentDayTokens > 0) {
+ // 旧数据:没有输入输出分离,假设70%为输出,30%为输入(基于一般对话比例)
+ totalOutputTokensToday += Math.round(currentDayTokens * 0.7);
+ totalInputTokensToday += Math.round(currentDayTokens * 0.3);
+ } else {
+ // 新数据:使用实际的输入输出分离
+ totalInputTokensToday += inputTokens;
+ totalOutputTokensToday += outputTokens;
+ }
+
+ // 添加cache token统计
+ totalCacheCreateTokensToday += cacheCreateTokens;
+ totalCacheReadTokensToday += cacheReadTokens;
+ }
+ }
+
+ // 获取今日创建的API Key数量(批量优化)
+ const allApiKeys = await this.client.keys('apikey:*');
+ let apiKeysCreatedToday = 0;
+
+ if (allApiKeys.length > 0) {
+ const pipeline = this.client.pipeline();
+ allApiKeys.forEach(key => pipeline.hget(key, 'createdAt'));
+ const results = await pipeline.exec();
+
+ for (const [error, createdAt] of results) {
+ if (!error && createdAt && createdAt.startsWith(today)) {
+ apiKeysCreatedToday++;
+ }
+ }
+ }
+
+ return {
+ requestsToday: totalRequestsToday,
+ tokensToday: totalTokensToday,
+ inputTokensToday: totalInputTokensToday,
+ outputTokensToday: totalOutputTokensToday,
+ cacheCreateTokensToday: totalCacheCreateTokensToday,
+ cacheReadTokensToday: totalCacheReadTokensToday,
+ apiKeysCreatedToday
+ };
+ } catch (error) {
+ console.error('Error getting today stats:', error);
+ return {
+ requestsToday: 0,
+ tokensToday: 0,
+ inputTokensToday: 0,
+ outputTokensToday: 0,
+ cacheCreateTokensToday: 0,
+ cacheReadTokensToday: 0,
+ apiKeysCreatedToday: 0
+ };
+ }
+ }
+
+ // 📈 获取系统总的平均RPM和TPM
+ async getSystemAverages() {
+ try {
+ const allApiKeys = await this.client.keys('apikey:*');
+ let totalRequests = 0;
+ let totalTokens = 0;
+ let totalInputTokens = 0;
+ let totalOutputTokens = 0;
+ let oldestCreatedAt = new Date();
+
+ // 批量获取所有usage数据和key数据,提高性能
+ const usageKeys = allApiKeys.map(key => `usage:${key.replace('apikey:', '')}`);
+ const pipeline = this.client.pipeline();
+
+ // 添加所有usage查询
+ usageKeys.forEach(key => pipeline.hgetall(key));
+ // 添加所有key数据查询
+ allApiKeys.forEach(key => pipeline.hgetall(key));
+
+ const results = await pipeline.exec();
+ const usageResults = results.slice(0, usageKeys.length);
+ const keyResults = results.slice(usageKeys.length);
+
+ for (let i = 0; i < allApiKeys.length; i++) {
+ const totalData = usageResults[i][1] || {};
+ const keyData = keyResults[i][1] || {};
+
+ totalRequests += parseInt(totalData.totalRequests) || 0;
+ totalTokens += parseInt(totalData.totalTokens) || 0;
+ totalInputTokens += parseInt(totalData.totalInputTokens) || 0;
+ totalOutputTokens += parseInt(totalData.totalOutputTokens) || 0;
+
+ const createdAt = keyData.createdAt ? new Date(keyData.createdAt) : new Date();
+ if (createdAt < oldestCreatedAt) {
+ oldestCreatedAt = createdAt;
+ }
+ }
+
+ const now = new Date();
+ // 保持与个人API Key计算一致的算法:按天计算然后转换为分钟
+ const daysSinceOldest = Math.max(1, Math.ceil((now - oldestCreatedAt) / (1000 * 60 * 60 * 24)));
+ const totalMinutes = daysSinceOldest * 24 * 60;
+
+ return {
+ systemRPM: Math.round((totalRequests / totalMinutes) * 100) / 100,
+ systemTPM: Math.round((totalTokens / totalMinutes) * 100) / 100,
+ totalInputTokens,
+ totalOutputTokens,
+ totalTokens
+ };
+ } catch (error) {
+ console.error('Error getting system averages:', error);
+ return {
+ systemRPM: 0,
+ systemTPM: 0,
+ totalInputTokens: 0,
+ totalOutputTokens: 0,
+ totalTokens: 0
+ };
+ }
+ }
+
+ // 🧹 清理过期数据
+ async cleanup() {
+ try {
+ const patterns = [
+ 'usage:daily:*',
+ 'ratelimit:*',
+ 'session:*',
+ 'oauth:*'
+ ];
+
+ for (const pattern of patterns) {
+ const keys = await this.client.keys(pattern);
+ const pipeline = this.client.pipeline();
+
+ for (const key of keys) {
+ const ttl = await this.client.ttl(key);
+ if (ttl === -1) { // 没有设置过期时间的键
+ if (key.startsWith('oauth:')) {
+ pipeline.expire(key, 600); // OAuth会话设置10分钟过期
+ } else {
+ pipeline.expire(key, 86400); // 其他设置1天过期
+ }
+ }
+ }
+
+ await pipeline.exec();
+ }
+
+ logger.info('🧹 Redis cleanup completed');
+ } catch (error) {
+ logger.error('❌ Redis cleanup failed:', error);
+ }
+ }
+}
+
+module.exports = new RedisClient();
\ No newline at end of file
diff --git a/src/routes/admin.js b/src/routes/admin.js
new file mode 100644
index 00000000..13a340c4
--- /dev/null
+++ b/src/routes/admin.js
@@ -0,0 +1,901 @@
+const express = require('express');
+const apiKeyService = require('../services/apiKeyService');
+const claudeAccountService = require('../services/claudeAccountService');
+const redis = require('../models/redis');
+const { authenticateAdmin } = require('../middleware/auth');
+const logger = require('../utils/logger');
+const oauthHelper = require('../utils/oauthHelper');
+const CostCalculator = require('../utils/costCalculator');
+const pricingService = require('../services/pricingService');
+
+const router = express.Router();
+
+// 🔑 API Keys 管理
+
+// 获取所有API Keys
+router.get('/api-keys', authenticateAdmin, async (req, res) => {
+ try {
+ const apiKeys = await apiKeyService.getAllApiKeys();
+ res.json({ success: true, data: apiKeys });
+ } catch (error) {
+ logger.error('❌ Failed to get API keys:', error);
+ res.status(500).json({ error: 'Failed to get API keys', message: error.message });
+ }
+});
+
+// 创建新的API Key
+router.post('/api-keys', authenticateAdmin, async (req, res) => {
+ try {
+ const {
+ name,
+ description,
+ tokenLimit,
+ requestLimit,
+ expiresAt,
+ claudeAccountId
+ } = req.body;
+
+ // 输入验证
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
+ return res.status(400).json({ error: 'Name is required and must be a non-empty string' });
+ }
+
+ if (name.length > 100) {
+ return res.status(400).json({ error: 'Name must be less than 100 characters' });
+ }
+
+ if (description && (typeof description !== 'string' || description.length > 500)) {
+ return res.status(400).json({ error: 'Description must be a string with less than 500 characters' });
+ }
+
+ if (tokenLimit && (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0)) {
+ return res.status(400).json({ error: 'Token limit must be a non-negative integer' });
+ }
+
+ if (requestLimit && (!Number.isInteger(Number(requestLimit)) || Number(requestLimit) < 0)) {
+ return res.status(400).json({ error: 'Request limit must be a non-negative integer' });
+ }
+
+ const newKey = await apiKeyService.generateApiKey({
+ name,
+ description,
+ tokenLimit,
+ requestLimit,
+ expiresAt,
+ claudeAccountId
+ });
+
+ logger.success(`🔑 Admin created new API key: ${name}`);
+ res.json({ success: true, data: newKey });
+ } catch (error) {
+ logger.error('❌ Failed to create API key:', error);
+ res.status(500).json({ error: 'Failed to create API key', message: error.message });
+ }
+});
+
+// 更新API Key
+router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
+ try {
+ const { keyId } = req.params;
+ const updates = req.body;
+
+ await apiKeyService.updateApiKey(keyId, updates);
+
+ logger.success(`📝 Admin updated API key: ${keyId}`);
+ res.json({ success: true, message: 'API key updated successfully' });
+ } catch (error) {
+ logger.error('❌ Failed to update API key:', error);
+ res.status(500).json({ error: 'Failed to update API key', message: error.message });
+ }
+});
+
+// 删除API Key
+router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
+ try {
+ const { keyId } = req.params;
+
+ await apiKeyService.deleteApiKey(keyId);
+
+ logger.success(`🗑️ Admin deleted API key: ${keyId}`);
+ res.json({ success: true, message: 'API key deleted successfully' });
+ } catch (error) {
+ logger.error('❌ Failed to delete API key:', error);
+ res.status(500).json({ error: 'Failed to delete API key', message: error.message });
+ }
+});
+
+// 🏢 Claude 账户管理
+
+// 生成OAuth授权URL
+router.post('/claude-accounts/generate-auth-url', authenticateAdmin, async (req, res) => {
+ try {
+ const { proxy } = req.body; // 接收代理配置
+ const oauthParams = await oauthHelper.generateOAuthParams();
+
+ // 将codeVerifier和state临时存储到Redis,用于后续验证
+ const sessionId = require('crypto').randomUUID();
+ await redis.setOAuthSession(sessionId, {
+ codeVerifier: oauthParams.codeVerifier,
+ state: oauthParams.state,
+ codeChallenge: oauthParams.codeChallenge,
+ proxy: proxy || null, // 存储代理配置
+ createdAt: new Date().toISOString(),
+ expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString() // 10分钟过期
+ });
+
+ logger.success('🔗 Generated OAuth authorization URL with proxy support');
+ res.json({
+ success: true,
+ data: {
+ authUrl: oauthParams.authUrl,
+ sessionId: sessionId,
+ instructions: [
+ '1. 复制上面的链接到浏览器中打开',
+ '2. 登录您的 Anthropic 账户',
+ '3. 同意应用权限',
+ '4. 复制浏览器地址栏中的完整 URL',
+ '5. 在添加账户表单中粘贴完整的回调 URL 和授权码'
+ ]
+ }
+ });
+ } catch (error) {
+ logger.error('❌ Failed to generate OAuth URL:', error);
+ res.status(500).json({ error: 'Failed to generate OAuth URL', message: error.message });
+ }
+});
+
+// 验证授权码并获取token
+router.post('/claude-accounts/exchange-code', authenticateAdmin, async (req, res) => {
+ try {
+ const { sessionId, authorizationCode, callbackUrl } = req.body;
+
+ if (!sessionId || (!authorizationCode && !callbackUrl)) {
+ return res.status(400).json({ error: 'Session ID and authorization code (or callback URL) are required' });
+ }
+
+ // 从Redis获取OAuth会话信息
+ const oauthSession = await redis.getOAuthSession(sessionId);
+ if (!oauthSession) {
+ return res.status(400).json({ error: 'Invalid or expired OAuth session' });
+ }
+
+ // 检查会话是否过期
+ if (new Date() > new Date(oauthSession.expiresAt)) {
+ await redis.deleteOAuthSession(sessionId);
+ return res.status(400).json({ error: 'OAuth session has expired, please generate a new authorization URL' });
+ }
+
+ // 统一处理授权码输入(可能是直接的code或完整的回调URL)
+ let finalAuthCode;
+ const inputValue = callbackUrl || authorizationCode;
+
+ try {
+ finalAuthCode = oauthHelper.parseCallbackUrl(inputValue);
+ } catch (parseError) {
+ return res.status(400).json({ error: 'Failed to parse authorization input', message: parseError.message });
+ }
+
+ // 交换访问令牌
+ const tokenData = await oauthHelper.exchangeCodeForTokens(
+ finalAuthCode,
+ oauthSession.codeVerifier,
+ oauthSession.state,
+ oauthSession.proxy // 传递代理配置
+ );
+
+ // 清理OAuth会话
+ await redis.deleteOAuthSession(sessionId);
+
+ logger.success('🎉 Successfully exchanged authorization code for tokens');
+ res.json({
+ success: true,
+ data: {
+ claudeAiOauth: tokenData
+ }
+ });
+ } catch (error) {
+ logger.error('❌ Failed to exchange authorization code:', {
+ error: error.message,
+ sessionId: req.body.sessionId,
+ // 不记录完整的授权码,只记录长度和前几个字符
+ codeLength: req.body.callbackUrl ? req.body.callbackUrl.length : (req.body.authorizationCode ? req.body.authorizationCode.length : 0),
+ codePrefix: req.body.callbackUrl ? req.body.callbackUrl.substring(0, 10) + '...' : (req.body.authorizationCode ? req.body.authorizationCode.substring(0, 10) + '...' : 'N/A')
+ });
+ res.status(500).json({ error: 'Failed to exchange authorization code', message: error.message });
+ }
+});
+
+// 获取所有Claude账户
+router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
+ try {
+ const accounts = await claudeAccountService.getAllAccounts();
+ res.json({ success: true, data: accounts });
+ } catch (error) {
+ logger.error('❌ Failed to get Claude accounts:', error);
+ res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message });
+ }
+});
+
+// 创建新的Claude账户
+router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
+ try {
+ const {
+ name,
+ description,
+ email,
+ password,
+ refreshToken,
+ claudeAiOauth,
+ proxy
+ } = req.body;
+
+ if (!name) {
+ return res.status(400).json({ error: 'Name is required' });
+ }
+
+ const newAccount = await claudeAccountService.createAccount({
+ name,
+ description,
+ email,
+ password,
+ refreshToken,
+ claudeAiOauth,
+ proxy
+ });
+
+ logger.success(`🏢 Admin created new Claude account: ${name}`);
+ res.json({ success: true, data: newAccount });
+ } catch (error) {
+ logger.error('❌ Failed to create Claude account:', error);
+ res.status(500).json({ error: 'Failed to create Claude account', message: error.message });
+ }
+});
+
+// 更新Claude账户
+router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => {
+ try {
+ const { accountId } = req.params;
+ const updates = req.body;
+
+ await claudeAccountService.updateAccount(accountId, updates);
+
+ logger.success(`📝 Admin updated Claude account: ${accountId}`);
+ res.json({ success: true, message: 'Claude account updated successfully' });
+ } catch (error) {
+ logger.error('❌ Failed to update Claude account:', error);
+ res.status(500).json({ error: 'Failed to update Claude account', message: error.message });
+ }
+});
+
+// 删除Claude账户
+router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => {
+ try {
+ const { accountId } = req.params;
+
+ await claudeAccountService.deleteAccount(accountId);
+
+ logger.success(`🗑️ Admin deleted Claude account: ${accountId}`);
+ res.json({ success: true, message: 'Claude account deleted successfully' });
+ } catch (error) {
+ logger.error('❌ Failed to delete Claude account:', error);
+ res.status(500).json({ error: 'Failed to delete Claude account', message: error.message });
+ }
+});
+
+// 刷新Claude账户token
+router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req, res) => {
+ try {
+ const { accountId } = req.params;
+
+ const result = await claudeAccountService.refreshAccountToken(accountId);
+
+ logger.success(`🔄 Admin refreshed token for Claude account: ${accountId}`);
+ res.json({ success: true, data: result });
+ } catch (error) {
+ logger.error('❌ Failed to refresh Claude account token:', error);
+ res.status(500).json({ error: 'Failed to refresh token', message: error.message });
+ }
+});
+
+// 📊 系统统计
+
+// 获取系统概览
+router.get('/dashboard', authenticateAdmin, async (req, res) => {
+ try {
+ const [, apiKeys, accounts, todayStats, systemAverages] = await Promise.all([
+ redis.getSystemStats(),
+ apiKeyService.getAllApiKeys(),
+ claudeAccountService.getAllAccounts(),
+ redis.getTodayStats(),
+ redis.getSystemAverages()
+ ]);
+
+ // 计算使用统计(包含cache tokens)
+ const totalTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
+ const totalRequestsUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
+ const totalInputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.inputTokens || 0), 0);
+ const totalOutputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.outputTokens || 0), 0);
+ const totalCacheCreateTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.cacheCreateTokens || 0), 0);
+ const totalCacheReadTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.cacheReadTokens || 0), 0);
+ const totalAllTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.allTokens || 0), 0);
+
+ const activeApiKeys = apiKeys.filter(key => key.isActive).length;
+ const activeAccounts = accounts.filter(acc => acc.isActive && acc.status === 'active').length;
+
+ const dashboard = {
+ overview: {
+ totalApiKeys: apiKeys.length,
+ activeApiKeys,
+ totalClaudeAccounts: accounts.length,
+ activeClaudeAccounts: activeAccounts,
+ totalTokensUsed,
+ totalRequestsUsed,
+ totalInputTokensUsed,
+ totalOutputTokensUsed,
+ totalCacheCreateTokensUsed,
+ totalCacheReadTokensUsed,
+ totalAllTokensUsed
+ },
+ recentActivity: {
+ apiKeysCreatedToday: todayStats.apiKeysCreatedToday,
+ requestsToday: todayStats.requestsToday,
+ tokensToday: todayStats.tokensToday,
+ inputTokensToday: todayStats.inputTokensToday,
+ outputTokensToday: todayStats.outputTokensToday,
+ cacheCreateTokensToday: todayStats.cacheCreateTokensToday || 0,
+ cacheReadTokensToday: todayStats.cacheReadTokensToday || 0
+ },
+ systemAverages: {
+ rpm: systemAverages.systemRPM,
+ tpm: systemAverages.systemTPM
+ },
+ systemHealth: {
+ redisConnected: redis.isConnected,
+ claudeAccountsHealthy: activeAccounts > 0,
+ uptime: process.uptime()
+ }
+ };
+
+ res.json({ success: true, data: dashboard });
+ } catch (error) {
+ logger.error('❌ Failed to get dashboard data:', error);
+ res.status(500).json({ error: 'Failed to get dashboard data', message: error.message });
+ }
+});
+
+// 获取使用统计
+router.get('/usage-stats', authenticateAdmin, async (req, res) => {
+ try {
+ const { period = 'daily' } = req.query; // daily, monthly
+
+ // 获取基础API Key统计
+ const apiKeys = await apiKeyService.getAllApiKeys();
+
+ const stats = apiKeys.map(key => ({
+ keyId: key.id,
+ keyName: key.name,
+ usage: key.usage
+ }));
+
+ res.json({ success: true, data: { period, stats } });
+ } catch (error) {
+ logger.error('❌ Failed to get usage stats:', error);
+ res.status(500).json({ error: 'Failed to get usage stats', message: error.message });
+ }
+});
+
+// 获取按模型的使用统计和费用
+router.get('/model-stats', authenticateAdmin, async (req, res) => {
+ try {
+ const { period = 'daily' } = req.query; // daily, monthly
+ const today = new Date().toISOString().split('T')[0];
+ const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
+
+ logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`);
+
+ const client = redis.getClientSafe();
+
+ // 获取所有模型的统计数据
+ const pattern = period === 'daily' ? `usage:model:daily:*:${today}` : `usage:model:monthly:*:${currentMonth}`;
+ logger.info(`📊 Searching pattern: ${pattern}`);
+
+ const keys = await client.keys(pattern);
+ logger.info(`📊 Found ${keys.length} matching keys:`, keys);
+
+ const modelStats = [];
+
+ for (const key of keys) {
+ const match = key.match(period === 'daily' ?
+ /usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ :
+ /usage:model:monthly:(.+):\d{4}-\d{2}$/
+ );
+
+ if (!match) {
+ logger.warn(`📊 Pattern mismatch for key: ${key}`);
+ continue;
+ }
+
+ const model = match[1];
+ const data = await client.hgetall(key);
+
+ logger.info(`📊 Model ${model} data:`, data);
+
+ if (data && Object.keys(data).length > 0) {
+ const usage = {
+ input_tokens: parseInt(data.inputTokens) || 0,
+ output_tokens: parseInt(data.outputTokens) || 0,
+ cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0,
+ cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0
+ };
+
+ // 计算费用
+ const costData = CostCalculator.calculateCost(usage, model);
+
+ modelStats.push({
+ model,
+ period,
+ requests: parseInt(data.requests) || 0,
+ inputTokens: usage.input_tokens,
+ outputTokens: usage.output_tokens,
+ cacheCreateTokens: usage.cache_creation_input_tokens,
+ cacheReadTokens: usage.cache_read_input_tokens,
+ allTokens: parseInt(data.allTokens) || 0,
+ usage: {
+ requests: parseInt(data.requests) || 0,
+ inputTokens: usage.input_tokens,
+ outputTokens: usage.output_tokens,
+ cacheCreateTokens: usage.cache_creation_input_tokens,
+ cacheReadTokens: usage.cache_read_input_tokens,
+ totalTokens: usage.input_tokens + usage.output_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens
+ },
+ costs: costData.costs,
+ formatted: costData.formatted,
+ pricing: costData.pricing
+ });
+ }
+ }
+
+ // 按总费用排序
+ modelStats.sort((a, b) => b.costs.total - a.costs.total);
+
+ logger.info(`📊 Returning ${modelStats.length} global model stats for period ${period}:`, modelStats);
+
+ res.json({ success: true, data: modelStats });
+ } catch (error) {
+ logger.error('❌ Failed to get model stats:', error);
+ res.status(500).json({ error: 'Failed to get model stats', message: error.message });
+ }
+});
+
+// 🔧 系统管理
+
+// 清理过期数据
+router.post('/cleanup', authenticateAdmin, async (req, res) => {
+ try {
+ const [expiredKeys, errorAccounts] = await Promise.all([
+ apiKeyService.cleanupExpiredKeys(),
+ claudeAccountService.cleanupErrorAccounts()
+ ]);
+
+ await redis.cleanup();
+
+ logger.success(`🧹 Admin triggered cleanup: ${expiredKeys} expired keys, ${errorAccounts} error accounts`);
+
+ res.json({
+ success: true,
+ message: 'Cleanup completed',
+ data: {
+ expiredKeysRemoved: expiredKeys,
+ errorAccountsReset: errorAccounts
+ }
+ });
+ } catch (error) {
+ logger.error('❌ Cleanup failed:', error);
+ res.status(500).json({ error: 'Cleanup failed', message: error.message });
+ }
+});
+
+// 获取使用趋势数据
+router.get('/usage-trend', authenticateAdmin, async (req, res) => {
+ try {
+ const { days = 7 } = req.query;
+ const daysCount = parseInt(days) || 7;
+ const client = redis.getClientSafe();
+
+ const trendData = [];
+ const today = new Date();
+
+ // 获取过去N天的数据
+ for (let i = 0; i < daysCount; i++) {
+ const date = new Date(today);
+ date.setDate(date.getDate() - i);
+ const dateStr = date.toISOString().split('T')[0];
+
+ // 汇总当天所有API Key的使用数据
+ const pattern = `usage:daily:*:${dateStr}`;
+ const keys = await client.keys(pattern);
+
+ let dayInputTokens = 0;
+ let dayOutputTokens = 0;
+ let dayRequests = 0;
+ let dayCacheCreateTokens = 0;
+ let dayCacheReadTokens = 0;
+ let dayCost = 0;
+
+ for (const key of keys) {
+ const data = await client.hgetall(key);
+ if (data) {
+ dayInputTokens += parseInt(data.inputTokens) || 0;
+ dayOutputTokens += parseInt(data.outputTokens) || 0;
+ dayRequests += parseInt(data.requests) || 0;
+ dayCacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
+ dayCacheReadTokens += parseInt(data.cacheReadTokens) || 0;
+ }
+ }
+
+ // 计算当天费用(使用通用模型价格估算)
+ const usage = {
+ input_tokens: dayInputTokens,
+ output_tokens: dayOutputTokens,
+ cache_creation_input_tokens: dayCacheCreateTokens,
+ cache_read_input_tokens: dayCacheReadTokens
+ };
+ const costResult = CostCalculator.calculateCost(usage, 'unknown');
+ dayCost = costResult.costs.total;
+
+ trendData.push({
+ date: dateStr,
+ inputTokens: dayInputTokens,
+ outputTokens: dayOutputTokens,
+ requests: dayRequests,
+ cacheCreateTokens: dayCacheCreateTokens,
+ cacheReadTokens: dayCacheReadTokens,
+ totalTokens: dayInputTokens + dayOutputTokens + dayCacheCreateTokens + dayCacheReadTokens,
+ cost: dayCost,
+ formattedCost: CostCalculator.formatCost(dayCost)
+ });
+ }
+
+ // 按日期正序排列
+ trendData.sort((a, b) => new Date(a.date) - new Date(b.date));
+
+ res.json({ success: true, data: trendData });
+ } catch (error) {
+ logger.error('❌ Failed to get usage trend:', error);
+ res.status(500).json({ error: 'Failed to get usage trend', message: error.message });
+ }
+});
+
+// 获取单个API Key的模型统计
+router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) => {
+ try {
+ const { keyId } = req.params;
+ const { period = 'monthly', startDate, endDate } = req.query;
+
+ logger.info(`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`);
+
+ const client = redis.getClientSafe();
+ const today = new Date().toISOString().split('T')[0];
+ const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
+
+ let searchPatterns = [];
+
+ if (period === 'custom' && startDate && endDate) {
+ // 自定义日期范围,生成多个日期的搜索模式
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ // 确保日期范围有效
+ if (start > end) {
+ return res.status(400).json({ error: 'Start date must be before or equal to end date' });
+ }
+
+ // 限制最大范围为31天
+ const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+ if (daysDiff > 31) {
+ return res.status(400).json({ error: 'Date range cannot exceed 31 days' });
+ }
+
+ // 生成日期范围内所有日期的搜索模式
+ for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
+ const dateStr = d.toISOString().split('T')[0];
+ searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`);
+ }
+
+ logger.info(`📊 Custom date range patterns: ${searchPatterns.length} days from ${startDate} to ${endDate}`);
+ } else {
+ // 原有的预设期间逻辑
+ const pattern = period === 'daily' ?
+ `usage:${keyId}:model:daily:*:${today}` :
+ `usage:${keyId}:model:monthly:*:${currentMonth}`;
+ searchPatterns = [pattern];
+ logger.info(`📊 Preset period pattern: ${pattern}`);
+ }
+
+ // 汇总所有匹配的数据
+ const modelStatsMap = new Map();
+ const modelStats = []; // 定义结果数组
+
+ for (const pattern of searchPatterns) {
+ const keys = await client.keys(pattern);
+ logger.info(`📊 Pattern ${pattern} found ${keys.length} keys`);
+
+ for (const key of keys) {
+ const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
+ key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/);
+
+ if (!match) {
+ logger.warn(`📊 Pattern mismatch for key: ${key}`);
+ continue;
+ }
+
+ const model = match[1];
+ const data = await client.hgetall(key);
+
+ if (data && Object.keys(data).length > 0) {
+ // 累加同一模型的数据
+ if (!modelStatsMap.has(model)) {
+ modelStatsMap.set(model, {
+ requests: 0,
+ inputTokens: 0,
+ outputTokens: 0,
+ cacheCreateTokens: 0,
+ cacheReadTokens: 0,
+ allTokens: 0
+ });
+ }
+
+ const stats = modelStatsMap.get(model);
+ stats.requests += parseInt(data.requests) || 0;
+ stats.inputTokens += parseInt(data.inputTokens) || 0;
+ stats.outputTokens += parseInt(data.outputTokens) || 0;
+ stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
+ stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0;
+ stats.allTokens += parseInt(data.allTokens) || 0;
+ }
+ }
+ }
+
+ // 将汇总的数据转换为最终结果
+ for (const [model, stats] of modelStatsMap) {
+ logger.info(`📊 Model ${model} aggregated data:`, stats);
+
+ const usage = {
+ input_tokens: stats.inputTokens,
+ output_tokens: stats.outputTokens,
+ cache_creation_input_tokens: stats.cacheCreateTokens,
+ cache_read_input_tokens: stats.cacheReadTokens
+ };
+
+ // 使用CostCalculator计算费用
+ const costData = CostCalculator.calculateCost(usage, model);
+
+ modelStats.push({
+ model,
+ requests: stats.requests,
+ inputTokens: stats.inputTokens,
+ outputTokens: stats.outputTokens,
+ cacheCreateTokens: stats.cacheCreateTokens,
+ cacheReadTokens: stats.cacheReadTokens,
+ allTokens: stats.allTokens,
+ // 添加费用信息
+ costs: costData.costs,
+ formatted: costData.formatted,
+ pricing: costData.pricing,
+ usingDynamicPricing: costData.usingDynamicPricing
+ });
+ }
+
+ // 如果没有找到模型级别的详细数据,尝试从汇总数据中生成展示
+ if (modelStats.length === 0) {
+ logger.info(`📊 No detailed model stats found, trying to get aggregate data for API key ${keyId}`);
+
+ // 尝试从API Keys列表中获取usage数据作为备选方案
+ try {
+ const apiKeys = await apiKeyService.getAllApiKeys();
+ const targetApiKey = apiKeys.find(key => key.id === keyId);
+
+ if (targetApiKey && targetApiKey.usage) {
+ logger.info(`📊 Found API key usage data from getAllApiKeys for ${keyId}:`, targetApiKey.usage);
+
+ // 从汇总数据创建展示条目
+ let usageData;
+ if (period === 'custom' || period === 'daily') {
+ // 对于自定义或日统计,使用daily数据或total数据
+ usageData = targetApiKey.usage.daily || targetApiKey.usage.total;
+ } else {
+ // 对于月统计,使用monthly数据或total数据
+ usageData = targetApiKey.usage.monthly || targetApiKey.usage.total;
+ }
+
+ if (usageData && usageData.allTokens > 0) {
+ const usage = {
+ input_tokens: usageData.inputTokens || 0,
+ output_tokens: usageData.outputTokens || 0,
+ cache_creation_input_tokens: usageData.cacheCreateTokens || 0,
+ cache_read_input_tokens: usageData.cacheReadTokens || 0
+ };
+
+ // 对于汇总数据,使用默认模型计算费用
+ const costData = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
+
+ modelStats.push({
+ model: '总体使用 (历史数据)',
+ requests: usageData.requests || 0,
+ inputTokens: usageData.inputTokens || 0,
+ outputTokens: usageData.outputTokens || 0,
+ cacheCreateTokens: usageData.cacheCreateTokens || 0,
+ cacheReadTokens: usageData.cacheReadTokens || 0,
+ allTokens: usageData.allTokens || 0,
+ // 添加费用信息
+ costs: costData.costs,
+ formatted: costData.formatted,
+ pricing: costData.pricing,
+ usingDynamicPricing: costData.usingDynamicPricing
+ });
+
+ logger.info('📊 Generated display data from API key usage stats');
+ } else {
+ logger.info(`📊 No usage data found for period ${period} in API key data`);
+ }
+ } else {
+ logger.info(`📊 API key ${keyId} not found or has no usage data`);
+ }
+ } catch (error) {
+ logger.error('❌ Error fetching API key usage data:', error);
+ }
+ }
+
+ // 按总token数降序排列
+ modelStats.sort((a, b) => b.allTokens - a.allTokens);
+
+ logger.info(`📊 Returning ${modelStats.length} model stats for API key ${keyId}:`, modelStats);
+
+ res.json({ success: true, data: modelStats });
+ } catch (error) {
+ logger.error('❌ Failed to get API key model stats:', error);
+ res.status(500).json({ error: 'Failed to get API key model stats', message: error.message });
+ }
+});
+
+
+// 计算总体使用费用
+router.get('/usage-costs', authenticateAdmin, async (req, res) => {
+ try {
+ const { period = 'all' } = req.query; // all, today, monthly
+
+ logger.info(`💰 Calculating usage costs for period: ${period}`);
+
+ // 获取所有API Keys的使用统计
+ const apiKeys = await apiKeyService.getAllApiKeys();
+
+ let totalCosts = {
+ inputCost: 0,
+ outputCost: 0,
+ cacheCreateCost: 0,
+ cacheReadCost: 0,
+ totalCost: 0
+ };
+
+ let modelCosts = {};
+
+ // 按模型统计费用
+ const client = redis.getClientSafe();
+ const today = new Date().toISOString().split('T')[0];
+ const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
+
+ let pattern;
+ if (period === 'today') {
+ pattern = `usage:model:daily:*:${today}`;
+ } else if (period === 'monthly') {
+ pattern = `usage:model:monthly:*:${currentMonth}`;
+ } else {
+ // 全部时间,使用API Key汇总数据
+ for (const apiKey of apiKeys) {
+ if (apiKey.usage && apiKey.usage.total) {
+ const usage = {
+ input_tokens: apiKey.usage.total.inputTokens || 0,
+ output_tokens: apiKey.usage.total.outputTokens || 0,
+ cache_creation_input_tokens: apiKey.usage.total.cacheCreateTokens || 0,
+ cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0
+ };
+
+ // 计算未知模型的费用(汇总数据)
+ const costResult = CostCalculator.calculateCost(usage, 'unknown');
+ totalCosts.inputCost += costResult.costs.input;
+ totalCosts.outputCost += costResult.costs.output;
+ totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
+ totalCosts.cacheReadCost += costResult.costs.cacheRead;
+ totalCosts.totalCost += costResult.costs.total;
+ }
+ }
+
+ res.json({
+ success: true,
+ data: {
+ period,
+ totalCosts: {
+ ...totalCosts,
+ formatted: {
+ inputCost: CostCalculator.formatCost(totalCosts.inputCost),
+ outputCost: CostCalculator.formatCost(totalCosts.outputCost),
+ cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost),
+ cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost),
+ totalCost: CostCalculator.formatCost(totalCosts.totalCost)
+ }
+ },
+ modelCosts: [],
+ pricingServiceStatus: pricingService.getStatus()
+ }
+ });
+ return;
+ }
+
+ // 对于今日或本月,从Redis获取详细的模型统计
+ const keys = await client.keys(pattern);
+
+ for (const key of keys) {
+ const match = key.match(period === 'today' ?
+ /usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ :
+ /usage:model:monthly:(.+):\d{4}-\d{2}$/
+ );
+
+ if (!match) continue;
+
+ const model = match[1];
+ const data = await client.hgetall(key);
+
+ if (data && Object.keys(data).length > 0) {
+ const usage = {
+ input_tokens: parseInt(data.inputTokens) || 0,
+ output_tokens: parseInt(data.outputTokens) || 0,
+ cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0,
+ cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0
+ };
+
+ const costResult = CostCalculator.calculateCost(usage, model);
+
+ // 累加总费用
+ totalCosts.inputCost += costResult.costs.input;
+ totalCosts.outputCost += costResult.costs.output;
+ totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
+ totalCosts.cacheReadCost += costResult.costs.cacheRead;
+ totalCosts.totalCost += costResult.costs.total;
+
+ // 记录模型费用
+ modelCosts[model] = {
+ model,
+ requests: parseInt(data.requests) || 0,
+ usage,
+ costs: costResult.costs,
+ formatted: costResult.formatted,
+ usingDynamicPricing: costResult.usingDynamicPricing
+ };
+ }
+ }
+
+ res.json({
+ success: true,
+ data: {
+ period,
+ totalCosts: {
+ ...totalCosts,
+ formatted: {
+ inputCost: CostCalculator.formatCost(totalCosts.inputCost),
+ outputCost: CostCalculator.formatCost(totalCosts.outputCost),
+ cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost),
+ cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost),
+ totalCost: CostCalculator.formatCost(totalCosts.totalCost)
+ }
+ },
+ modelCosts: Object.values(modelCosts).sort((a, b) => b.costs.total - a.costs.total),
+ pricingServiceStatus: pricingService.getStatus()
+ }
+ });
+ } catch (error) {
+ logger.error('❌ Failed to calculate usage costs:', error);
+ res.status(500).json({ error: 'Failed to calculate usage costs', message: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/routes/api.js b/src/routes/api.js
new file mode 100644
index 00000000..af6af993
--- /dev/null
+++ b/src/routes/api.js
@@ -0,0 +1,225 @@
+const express = require('express');
+const claudeRelayService = require('../services/claudeRelayService');
+const apiKeyService = require('../services/apiKeyService');
+const { authenticateApiKey } = require('../middleware/auth');
+const logger = require('../utils/logger');
+
+const router = express.Router();
+
+// 🚀 Claude API messages 端点
+router.post('/v1/messages', authenticateApiKey, async (req, res) => {
+ try {
+ const startTime = Date.now();
+
+ // 严格的输入验证
+ if (!req.body || typeof req.body !== 'object') {
+ return res.status(400).json({
+ error: 'Invalid request',
+ message: 'Request body must be a valid JSON object'
+ });
+ }
+
+ if (!req.body.messages || !Array.isArray(req.body.messages)) {
+ return res.status(400).json({
+ error: 'Invalid request',
+ message: 'Missing or invalid field: messages (must be an array)'
+ });
+ }
+
+ if (req.body.messages.length === 0) {
+ return res.status(400).json({
+ error: 'Invalid request',
+ message: 'Messages array cannot be empty'
+ });
+ }
+
+ // 检查是否为流式请求
+ const isStream = req.body.stream === true;
+
+ logger.api(`🚀 Processing ${isStream ? 'stream' : 'non-stream'} request for key: ${req.apiKey.name}`);
+
+ if (isStream) {
+ // 流式响应 - 只使用官方真实usage数据
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+
+ let usageDataCaptured = false;
+
+ // 使用自定义流处理器来捕获usage数据
+ await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, (usageData) => {
+ // 回调函数:当检测到完整usage数据时记录真实token使用量
+ logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
+
+ if (usageData && usageData.input_tokens !== undefined && usageData.output_tokens !== undefined) {
+ const inputTokens = usageData.input_tokens || 0;
+ const outputTokens = usageData.output_tokens || 0;
+ const cacheCreateTokens = usageData.cache_creation_input_tokens || 0;
+ const cacheReadTokens = usageData.cache_read_input_tokens || 0;
+ const model = usageData.model || 'unknown';
+
+ // 记录真实的token使用量(包含模型信息和所有4种token)
+ apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model).catch(error => {
+ logger.error('❌ Failed to record stream usage:', error);
+ });
+
+ usageDataCaptured = true;
+ logger.api(`📊 Stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
+ } else {
+ logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
+ }
+ });
+
+ // 流式请求完成后 - 如果没有捕获到usage数据,记录警告但不进行估算
+ setTimeout(() => {
+ if (!usageDataCaptured) {
+ logger.warn('⚠️ No usage data captured from SSE stream - no statistics recorded (official data only)');
+ }
+ }, 1000); // 1秒后检查
+ } else {
+ // 非流式响应 - 只使用官方真实usage数据
+ logger.info('📄 Starting non-streaming request', {
+ apiKeyId: req.apiKey.id,
+ apiKeyName: req.apiKey.name
+ });
+
+ const response = await claudeRelayService.relayRequest(req.body, req.apiKey);
+
+ logger.info('📡 Claude API response received', {
+ statusCode: response.statusCode,
+ headers: JSON.stringify(response.headers),
+ bodyLength: response.body ? response.body.length : 0
+ });
+
+ res.status(response.statusCode);
+
+ // 设置响应头
+ Object.keys(response.headers).forEach(key => {
+ if (key.toLowerCase() !== 'content-encoding') {
+ res.setHeader(key, response.headers[key]);
+ }
+ });
+
+ let usageRecorded = false;
+
+ // 尝试解析JSON响应并提取usage信息
+ try {
+ const jsonData = JSON.parse(response.body);
+
+ logger.info('📊 Parsed Claude API response:', JSON.stringify(jsonData, null, 2));
+
+ // 从Claude API响应中提取usage信息(完整的token分类体系)
+ if (jsonData.usage && jsonData.usage.input_tokens !== undefined && jsonData.usage.output_tokens !== undefined) {
+ const inputTokens = jsonData.usage.input_tokens || 0;
+ const outputTokens = jsonData.usage.output_tokens || 0;
+ const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0;
+ const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0;
+ const model = jsonData.model || req.body.model || 'unknown';
+
+ // 记录真实的token使用量(包含模型信息和所有4种token)
+ await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
+
+ usageRecorded = true;
+ logger.api(`📊 Non-stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
+ } else {
+ logger.warn('⚠️ No usage data found in Claude API JSON response');
+ }
+
+ res.json(jsonData);
+ } catch (parseError) {
+ logger.warn('⚠️ Failed to parse Claude API response as JSON:', parseError.message);
+ logger.info('📄 Raw response body:', response.body);
+ res.send(response.body);
+ }
+
+ // 如果没有记录usage,只记录警告,不进行估算
+ if (!usageRecorded) {
+ logger.warn('⚠️ No usage data recorded for non-stream request - no statistics recorded (official data only)');
+ }
+ }
+
+ const duration = Date.now() - startTime;
+ logger.api(`✅ Request completed in ${duration}ms for key: ${req.apiKey.name}`);
+
+ } catch (error) {
+ logger.error('❌ Claude relay error:', error);
+
+ if (!res.headersSent) {
+ res.status(500).json({
+ error: 'Relay service error',
+ message: error.message
+ });
+ }
+ }
+});
+
+// 🏥 健康检查端点
+router.get('/health', async (req, res) => {
+ try {
+ const healthStatus = await claudeRelayService.healthCheck();
+
+ res.status(healthStatus.healthy ? 200 : 503).json({
+ status: healthStatus.healthy ? 'healthy' : 'unhealthy',
+ service: 'claude-relay-service',
+ version: '1.0.0',
+ ...healthStatus
+ });
+ } catch (error) {
+ logger.error('❌ Health check error:', error);
+ res.status(503).json({
+ status: 'unhealthy',
+ service: 'claude-relay-service',
+ error: error.message,
+ timestamp: new Date().toISOString()
+ });
+ }
+});
+
+// 📊 API Key状态检查端点
+router.get('/v1/key-info', authenticateApiKey, async (req, res) => {
+ try {
+ const usage = await apiKeyService.getUsageStats(req.apiKey.id);
+
+ res.json({
+ keyInfo: {
+ id: req.apiKey.id,
+ name: req.apiKey.name,
+ tokenLimit: req.apiKey.tokenLimit,
+ requestLimit: req.apiKey.requestLimit,
+ usage
+ },
+ timestamp: new Date().toISOString()
+ });
+ } catch (error) {
+ logger.error('❌ Key info error:', error);
+ res.status(500).json({
+ error: 'Failed to get key info',
+ message: error.message
+ });
+ }
+});
+
+// 📈 使用统计端点
+router.get('/v1/usage', authenticateApiKey, async (req, res) => {
+ try {
+ const usage = await apiKeyService.getUsageStats(req.apiKey.id);
+
+ res.json({
+ usage,
+ limits: {
+ tokens: req.apiKey.tokenLimit,
+ requests: req.apiKey.requestLimit
+ },
+ timestamp: new Date().toISOString()
+ });
+ } catch (error) {
+ logger.error('❌ Usage stats error:', error);
+ res.status(500).json({
+ error: 'Failed to get usage stats',
+ message: error.message
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/routes/web.js b/src/routes/web.js
new file mode 100644
index 00000000..0a0420c6
--- /dev/null
+++ b/src/routes/web.js
@@ -0,0 +1,202 @@
+const express = require('express');
+const bcrypt = require('bcryptjs');
+const crypto = require('crypto');
+const path = require('path');
+const fs = require('fs');
+const redis = require('../models/redis');
+const logger = require('../utils/logger');
+const config = require('../../config/config');
+
+const router = express.Router();
+
+// 🏠 服务静态文件
+router.use('/assets', express.static(path.join(__dirname, '../../web/assets')));
+
+// 🔒 Web管理界面文件白名单 - 仅允许这些特定文件
+const ALLOWED_FILES = {
+ 'index.html': {
+ path: path.join(__dirname, '../../web/admin/index.html'),
+ contentType: 'text/html; charset=utf-8'
+ },
+ 'app.js': {
+ path: path.join(__dirname, '../../web/admin/app.js'),
+ contentType: 'application/javascript; charset=utf-8'
+ },
+ 'style.css': {
+ path: path.join(__dirname, '../../web/admin/style.css'),
+ contentType: 'text/css; charset=utf-8'
+ }
+};
+
+// 🛡️ 安全文件服务函数
+function serveWhitelistedFile(req, res, filename) {
+ const fileConfig = ALLOWED_FILES[filename];
+
+ if (!fileConfig) {
+ logger.security(`🚨 Attempted access to non-whitelisted file: ${filename}`);
+ return res.status(404).json({ error: 'File not found' });
+ }
+
+ try {
+ // 检查文件是否存在
+ if (!fs.existsSync(fileConfig.path)) {
+ logger.error(`❌ Whitelisted file not found: ${fileConfig.path}`);
+ return res.status(404).json({ error: 'File not found' });
+ }
+
+ // 读取并返回文件内容
+ const content = fs.readFileSync(fileConfig.path, 'utf8');
+ res.setHeader('Content-Type', fileConfig.contentType);
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ res.send(content);
+
+ logger.info(`📄 Served whitelisted file: ${filename}`);
+ } catch (error) {
+ logger.error(`❌ Error serving file ${filename}:`, error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+}
+
+// 🔐 管理员登录
+router.post('/auth/login', async (req, res) => {
+ try {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).json({
+ error: 'Missing credentials',
+ message: 'Username and password are required'
+ });
+ }
+
+ // 从Redis获取管理员信息
+ const adminData = await redis.getSession('admin_credentials');
+
+ if (!adminData || Object.keys(adminData).length === 0) {
+ return res.status(401).json({
+ error: 'Invalid credentials',
+ message: 'Invalid username or password'
+ });
+ }
+
+ // 验证用户名和密码
+ const isValidUsername = adminData.username === username;
+ const isValidPassword = await bcrypt.compare(password, adminData.passwordHash);
+
+ if (!isValidUsername || !isValidPassword) {
+ logger.security(`🔒 Failed login attempt for username: ${username}`);
+ return res.status(401).json({
+ error: 'Invalid credentials',
+ message: 'Invalid username or password'
+ });
+ }
+
+ // 生成会话token
+ const sessionId = crypto.randomBytes(32).toString('hex');
+
+ // 存储会话
+ const sessionData = {
+ username: adminData.username,
+ loginTime: new Date().toISOString(),
+ lastActivity: new Date().toISOString()
+ };
+
+ await redis.setSession(sessionId, sessionData, config.security.adminSessionTimeout);
+
+ // 更新最后登录时间
+ adminData.lastLogin = new Date().toISOString();
+ await redis.setSession('admin_credentials', adminData);
+
+ logger.success(`🔐 Admin login successful: ${username}`);
+
+ res.json({
+ success: true,
+ token: sessionId,
+ expiresIn: config.security.adminSessionTimeout
+ });
+
+ } catch (error) {
+ logger.error('❌ Login error:', error);
+ res.status(500).json({
+ error: 'Login failed',
+ message: 'Internal server error'
+ });
+ }
+});
+
+// 🚪 管理员登出
+router.post('/auth/logout', async (req, res) => {
+ try {
+ const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken;
+
+ if (token) {
+ await redis.deleteSession(token);
+ logger.success('🚪 Admin logout successful');
+ }
+
+ res.json({ success: true, message: 'Logout successful' });
+ } catch (error) {
+ logger.error('❌ Logout error:', error);
+ res.status(500).json({
+ error: 'Logout failed',
+ message: 'Internal server error'
+ });
+ }
+});
+
+// 🔄 刷新token
+router.post('/auth/refresh', async (req, res) => {
+ try {
+ const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken;
+
+ if (!token) {
+ return res.status(401).json({
+ error: 'No token provided',
+ message: 'Authentication required'
+ });
+ }
+
+ const sessionData = await redis.getSession(token);
+
+ if (!sessionData) {
+ return res.status(401).json({
+ error: 'Invalid token',
+ message: 'Session expired or invalid'
+ });
+ }
+
+ // 更新最后活动时间
+ sessionData.lastActivity = new Date().toISOString();
+ await redis.setSession(token, sessionData, config.security.adminSessionTimeout);
+
+ res.json({
+ success: true,
+ token: token,
+ expiresIn: config.security.adminSessionTimeout
+ });
+
+ } catch (error) {
+ logger.error('❌ Token refresh error:', error);
+ res.status(500).json({
+ error: 'Token refresh failed',
+ message: 'Internal server error'
+ });
+ }
+});
+
+// 🌐 Web管理界面路由 - 使用固定白名单
+router.get('/', (req, res) => {
+ serveWhitelistedFile(req, res, 'index.html');
+});
+
+router.get('/app.js', (req, res) => {
+ serveWhitelistedFile(req, res, 'app.js');
+});
+
+router.get('/style.css', (req, res) => {
+ serveWhitelistedFile(req, res, 'style.css');
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js
new file mode 100644
index 00000000..552d7421
--- /dev/null
+++ b/src/services/apiKeyService.js
@@ -0,0 +1,271 @@
+const crypto = require('crypto');
+const { v4: uuidv4 } = require('uuid');
+const config = require('../../config/config');
+const redis = require('../models/redis');
+const logger = require('../utils/logger');
+
+class ApiKeyService {
+ constructor() {
+ this.prefix = config.security.apiKeyPrefix;
+ }
+
+ // 🔑 生成新的API Key
+ async generateApiKey(options = {}) {
+ const {
+ name = 'Unnamed Key',
+ description = '',
+ tokenLimit = config.limits.defaultTokenLimit,
+ requestLimit = config.limits.defaultRequestLimit,
+ expiresAt = null,
+ claudeAccountId = null,
+ isActive = true
+ } = options;
+
+ // 生成简单的API Key (64字符十六进制)
+ const apiKey = `${this.prefix}${this._generateSecretKey()}`;
+ const keyId = uuidv4();
+ const hashedKey = this._hashApiKey(apiKey);
+
+ const keyData = {
+ id: keyId,
+ name,
+ description,
+ apiKey: hashedKey,
+ tokenLimit: String(tokenLimit ?? 0),
+ requestLimit: String(requestLimit ?? 0),
+ isActive: String(isActive),
+ claudeAccountId: claudeAccountId || '',
+ createdAt: new Date().toISOString(),
+ lastUsedAt: '',
+ expiresAt: expiresAt || '',
+ createdBy: 'admin' // 可以根据需要扩展用户系统
+ };
+
+ // 保存API Key数据并建立哈希映射
+ await redis.setApiKey(keyId, keyData, hashedKey);
+
+ logger.success(`🔑 Generated new API key: ${name} (${keyId})`);
+
+ return {
+ id: keyId,
+ apiKey, // 只在创建时返回完整的key
+ name: keyData.name,
+ description: keyData.description,
+ tokenLimit: parseInt(keyData.tokenLimit),
+ requestLimit: parseInt(keyData.requestLimit),
+ isActive: keyData.isActive === 'true',
+ claudeAccountId: keyData.claudeAccountId,
+ createdAt: keyData.createdAt,
+ expiresAt: keyData.expiresAt,
+ createdBy: keyData.createdBy
+ };
+ }
+
+ // 🔍 验证API Key
+ async validateApiKey(apiKey) {
+ try {
+ if (!apiKey || !apiKey.startsWith(this.prefix)) {
+ return { valid: false, error: 'Invalid API key format' };
+ }
+
+ // 计算API Key的哈希值
+ const hashedKey = this._hashApiKey(apiKey);
+
+ // 通过哈希值直接查找API Key(性能优化)
+ const keyData = await redis.findApiKeyByHash(hashedKey);
+
+ if (!keyData) {
+ return { valid: false, error: 'API key not found' };
+ }
+
+ // 检查是否激活
+ if (keyData.isActive !== 'true') {
+ return { valid: false, error: 'API key is disabled' };
+ }
+
+ // 检查是否过期
+ if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) {
+ return { valid: false, error: 'API key has expired' };
+ }
+
+ // 检查使用限制
+ const usage = await redis.getUsageStats(keyData.id);
+ const tokenLimit = parseInt(keyData.tokenLimit);
+ const requestLimit = parseInt(keyData.requestLimit);
+
+ if (tokenLimit > 0 && usage.total.tokens >= tokenLimit) {
+ return { valid: false, error: 'Token limit exceeded' };
+ }
+
+ if (requestLimit > 0 && usage.total.requests >= requestLimit) {
+ return { valid: false, error: 'Request limit exceeded' };
+ }
+
+ // 更新最后使用时间(优化:只在实际API调用时更新,而不是验证时)
+ // 注意:lastUsedAt的更新已移至recordUsage方法中
+
+ logger.api(`🔓 API key validated successfully: ${keyData.id}`);
+
+ return {
+ valid: true,
+ keyData: {
+ id: keyData.id,
+ name: keyData.name,
+ claudeAccountId: keyData.claudeAccountId,
+ tokenLimit: parseInt(keyData.tokenLimit),
+ requestLimit: parseInt(keyData.requestLimit),
+ usage
+ }
+ };
+ } catch (error) {
+ logger.error('❌ API key validation error:', error);
+ return { valid: false, error: 'Internal validation error' };
+ }
+ }
+
+ // 📋 获取所有API Keys
+ async getAllApiKeys() {
+ try {
+ const apiKeys = await redis.getAllApiKeys();
+
+ // 为每个key添加使用统计
+ for (const key of apiKeys) {
+ key.usage = await redis.getUsageStats(key.id);
+ key.tokenLimit = parseInt(key.tokenLimit);
+ key.requestLimit = parseInt(key.requestLimit);
+ key.isActive = key.isActive === 'true';
+ delete key.apiKey; // 不返回哈希后的key
+ }
+
+ return apiKeys;
+ } catch (error) {
+ logger.error('❌ Failed to get API keys:', error);
+ throw error;
+ }
+ }
+
+ // 📝 更新API Key
+ async updateApiKey(keyId, updates) {
+ try {
+ const keyData = await redis.getApiKey(keyId);
+ if (!keyData || Object.keys(keyData).length === 0) {
+ throw new Error('API key not found');
+ }
+
+ // 允许更新的字段
+ const allowedUpdates = ['name', 'description', 'tokenLimit', 'requestLimit', 'isActive', 'claudeAccountId', 'expiresAt'];
+ const updatedData = { ...keyData };
+
+ for (const [field, value] of Object.entries(updates)) {
+ if (allowedUpdates.includes(field)) {
+ updatedData[field] = (value != null ? value : '').toString();
+ }
+ }
+
+ updatedData.updatedAt = new Date().toISOString();
+
+ // 更新时不需要重新建立哈希映射,因为API Key本身没有变化
+ await redis.setApiKey(keyId, updatedData);
+
+ logger.success(`📝 Updated API key: ${keyId}`);
+
+ return { success: true };
+ } catch (error) {
+ logger.error('❌ Failed to update API key:', error);
+ throw error;
+ }
+ }
+
+ // 🗑️ 删除API Key
+ async deleteApiKey(keyId) {
+ try {
+ const result = await redis.deleteApiKey(keyId);
+
+ if (result === 0) {
+ throw new Error('API key not found');
+ }
+
+ logger.success(`🗑️ Deleted API key: ${keyId}`);
+
+ return { success: true };
+ } catch (error) {
+ logger.error('❌ Failed to delete API key:', error);
+ throw error;
+ }
+ }
+
+ // 📊 记录使用情况(支持缓存token)
+ async recordUsage(keyId, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
+ try {
+ const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
+ await redis.incrementTokenUsage(keyId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
+
+ // 更新最后使用时间(性能优化:只在实际使用时更新)
+ const keyData = await redis.getApiKey(keyId);
+ if (keyData && Object.keys(keyData).length > 0) {
+ keyData.lastUsedAt = new Date().toISOString();
+ // 使用记录时不需要重新建立哈希映射
+ await redis.setApiKey(keyId, keyData);
+ }
+
+ const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`];
+ if (cacheCreateTokens > 0) logParts.push(`Cache Create: ${cacheCreateTokens}`);
+ if (cacheReadTokens > 0) logParts.push(`Cache Read: ${cacheReadTokens}`);
+ logParts.push(`Total: ${totalTokens} tokens`);
+
+ logger.database(`📊 Recorded usage: ${keyId} - ${logParts.join(', ')}`);
+ } catch (error) {
+ logger.error('❌ Failed to record usage:', error);
+ }
+ }
+
+ // 🔐 生成密钥
+ _generateSecretKey() {
+ return crypto.randomBytes(32).toString('hex');
+ }
+
+ // 🔒 哈希API Key
+ _hashApiKey(apiKey) {
+ return crypto.createHash('sha256').update(apiKey + config.security.encryptionKey).digest('hex');
+ }
+
+ // 📈 获取使用统计
+ async getUsageStats(keyId) {
+ return await redis.getUsageStats(keyId);
+ }
+
+ // 🚦 检查速率限制
+ async checkRateLimit(keyId, limit = null) {
+ const rateLimit = limit || config.rateLimit.maxRequests;
+ const window = Math.floor(config.rateLimit.windowMs / 1000);
+
+ return await redis.checkRateLimit(`apikey:${keyId}`, rateLimit, window);
+ }
+
+ // 🧹 清理过期的API Keys
+ async cleanupExpiredKeys() {
+ try {
+ const apiKeys = await redis.getAllApiKeys();
+ const now = new Date();
+ let cleanedCount = 0;
+
+ for (const key of apiKeys) {
+ if (key.expiresAt && new Date(key.expiresAt) < now) {
+ await redis.deleteApiKey(key.id);
+ cleanedCount++;
+ }
+ }
+
+ if (cleanedCount > 0) {
+ logger.success(`🧹 Cleaned up ${cleanedCount} expired API keys`);
+ }
+
+ return cleanedCount;
+ } catch (error) {
+ logger.error('❌ Failed to cleanup expired keys:', error);
+ return 0;
+ }
+ }
+}
+
+module.exports = new ApiKeyService();
\ No newline at end of file
diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js
new file mode 100644
index 00000000..8498bf1b
--- /dev/null
+++ b/src/services/claudeAccountService.js
@@ -0,0 +1,452 @@
+const { v4: uuidv4 } = require('uuid');
+const crypto = require('crypto');
+const { SocksProxyAgent } = require('socks-proxy-agent');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const axios = require('axios');
+const redis = require('../models/redis');
+const logger = require('../utils/logger');
+const config = require('../../config/config');
+
+class ClaudeAccountService {
+ constructor() {
+ this.claudeApiUrl = 'https://console.anthropic.com/v1/oauth/token';
+ this.claudeOauthClientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
+
+ // 加密相关常量
+ this.ENCRYPTION_ALGORITHM = 'aes-256-cbc';
+ this.ENCRYPTION_SALT = 'salt';
+ }
+
+ // 🏢 创建Claude账户
+ async createAccount(options = {}) {
+ const {
+ name = 'Unnamed Account',
+ description = '',
+ email = '',
+ password = '',
+ refreshToken = '',
+ claudeAiOauth = null, // Claude标准格式的OAuth数据
+ proxy = null, // { type: 'socks5', host: 'localhost', port: 1080, username: '', password: '' }
+ isActive = true
+ } = options;
+
+ const accountId = uuidv4();
+
+ let accountData;
+
+ if (claudeAiOauth) {
+ // 使用Claude标准格式的OAuth数据
+ accountData = {
+ id: accountId,
+ name,
+ description,
+ email: this._encryptSensitiveData(email),
+ password: this._encryptSensitiveData(password),
+ claudeAiOauth: this._encryptSensitiveData(JSON.stringify(claudeAiOauth)),
+ accessToken: this._encryptSensitiveData(claudeAiOauth.accessToken),
+ refreshToken: this._encryptSensitiveData(claudeAiOauth.refreshToken),
+ expiresAt: claudeAiOauth.expiresAt.toString(),
+ scopes: claudeAiOauth.scopes.join(' '),
+ proxy: proxy ? JSON.stringify(proxy) : '',
+ isActive: isActive.toString(),
+ createdAt: new Date().toISOString(),
+ lastUsedAt: '',
+ lastRefreshAt: '',
+ status: 'active', // 有OAuth数据的账户直接设为active
+ errorMessage: ''
+ };
+ } else {
+ // 兼容旧格式
+ accountData = {
+ id: accountId,
+ name,
+ description,
+ email: this._encryptSensitiveData(email),
+ password: this._encryptSensitiveData(password),
+ refreshToken: this._encryptSensitiveData(refreshToken),
+ accessToken: '',
+ expiresAt: '',
+ scopes: '',
+ proxy: proxy ? JSON.stringify(proxy) : '',
+ isActive: isActive.toString(),
+ createdAt: new Date().toISOString(),
+ lastUsedAt: '',
+ lastRefreshAt: '',
+ status: 'created', // created, active, expired, error
+ errorMessage: ''
+ };
+ }
+
+ await redis.setClaudeAccount(accountId, accountData);
+
+ logger.success(`🏢 Created Claude account: ${name} (${accountId})`);
+
+ return {
+ id: accountId,
+ name,
+ description,
+ email,
+ isActive,
+ proxy,
+ status: accountData.status,
+ createdAt: accountData.createdAt,
+ expiresAt: accountData.expiresAt,
+ scopes: claudeAiOauth ? claudeAiOauth.scopes : []
+ };
+ }
+
+ // 🔄 刷新Claude账户token
+ async refreshAccountToken(accountId) {
+ try {
+ const accountData = await redis.getClaudeAccount(accountId);
+
+ if (!accountData || Object.keys(accountData).length === 0) {
+ throw new Error('Account not found');
+ }
+
+ const refreshToken = this._decryptSensitiveData(accountData.refreshToken);
+
+ if (!refreshToken) {
+ throw new Error('No refresh token available');
+ }
+
+ // 创建代理agent
+ const agent = this._createProxyAgent(accountData.proxy);
+
+ const response = await axios.post(this.claudeApiUrl, {
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: this.claudeOauthClientId
+ }, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json, text/plain, */*',
+ 'User-Agent': 'claude-relay-service/1.0.0'
+ },
+ httpsAgent: agent,
+ timeout: 30000
+ });
+
+ if (response.status === 200) {
+ const { access_token, refresh_token, expires_in } = response.data;
+
+ // 更新账户数据
+ accountData.accessToken = this._encryptSensitiveData(access_token);
+ accountData.refreshToken = this._encryptSensitiveData(refresh_token);
+ accountData.expiresAt = (Date.now() + (expires_in * 1000)).toString();
+ accountData.lastRefreshAt = new Date().toISOString();
+ accountData.status = 'active';
+ accountData.errorMessage = '';
+
+ await redis.setClaudeAccount(accountId, accountData);
+
+ logger.success(`🔄 Refreshed token for account: ${accountData.name} (${accountId})`);
+
+ return {
+ success: true,
+ accessToken: access_token,
+ expiresAt: accountData.expiresAt
+ };
+ } else {
+ throw new Error(`Token refresh failed with status: ${response.status}`);
+ }
+ } catch (error) {
+ logger.error(`❌ Failed to refresh token for account ${accountId}:`, error);
+
+ // 更新错误状态
+ const accountData = await redis.getClaudeAccount(accountId);
+ if (accountData) {
+ accountData.status = 'error';
+ accountData.errorMessage = error.message;
+ await redis.setClaudeAccount(accountId, accountData);
+ }
+
+ throw error;
+ }
+ }
+
+ // 🎯 获取有效的访问token
+ async getValidAccessToken(accountId) {
+ try {
+ const accountData = await redis.getClaudeAccount(accountId);
+
+ if (!accountData || Object.keys(accountData).length === 0) {
+ throw new Error('Account not found');
+ }
+
+ if (accountData.isActive !== 'true') {
+ throw new Error('Account is disabled');
+ }
+
+ // 检查token是否过期
+ const expiresAt = parseInt(accountData.expiresAt);
+ const now = Date.now();
+
+ if (!expiresAt || now >= (expiresAt - 10000)) { // 10秒提前刷新
+ logger.info(`🔄 Token expired/expiring for account ${accountId}, refreshing...`);
+ const refreshResult = await this.refreshAccountToken(accountId);
+ return refreshResult.accessToken;
+ }
+
+ const accessToken = this._decryptSensitiveData(accountData.accessToken);
+
+ if (!accessToken) {
+ throw new Error('No access token available');
+ }
+
+ // 更新最后使用时间
+ accountData.lastUsedAt = new Date().toISOString();
+ await redis.setClaudeAccount(accountId, accountData);
+
+ return accessToken;
+ } catch (error) {
+ logger.error(`❌ Failed to get valid access token for account ${accountId}:`, error);
+ throw error;
+ }
+ }
+
+ // 📋 获取所有Claude账户
+ async getAllAccounts() {
+ try {
+ const accounts = await redis.getAllClaudeAccounts();
+
+ // 处理返回数据,移除敏感信息
+ return accounts.map(account => ({
+ id: account.id,
+ name: account.name,
+ description: account.description,
+ email: account.email ? this._maskEmail(this._decryptSensitiveData(account.email)) : '',
+ isActive: account.isActive === 'true',
+ proxy: account.proxy ? JSON.parse(account.proxy) : null,
+ status: account.status,
+ errorMessage: account.errorMessage,
+ createdAt: account.createdAt,
+ lastUsedAt: account.lastUsedAt,
+ lastRefreshAt: account.lastRefreshAt,
+ expiresAt: account.expiresAt
+ }));
+ } catch (error) {
+ logger.error('❌ Failed to get Claude accounts:', error);
+ throw error;
+ }
+ }
+
+ // 📝 更新Claude账户
+ async updateAccount(accountId, updates) {
+ try {
+ const accountData = await redis.getClaudeAccount(accountId);
+
+ if (!accountData || Object.keys(accountData).length === 0) {
+ throw new Error('Account not found');
+ }
+
+ const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive'];
+ const updatedData = { ...accountData };
+
+ for (const [field, value] of Object.entries(updates)) {
+ if (allowedUpdates.includes(field)) {
+ if (['email', 'password', 'refreshToken'].includes(field)) {
+ updatedData[field] = this._encryptSensitiveData(value);
+ } else if (field === 'proxy') {
+ updatedData[field] = value ? JSON.stringify(value) : '';
+ } else {
+ updatedData[field] = value.toString();
+ }
+ }
+ }
+
+ updatedData.updatedAt = new Date().toISOString();
+
+ await redis.setClaudeAccount(accountId, updatedData);
+
+ logger.success(`📝 Updated Claude account: ${accountId}`);
+
+ return { success: true };
+ } catch (error) {
+ logger.error('❌ Failed to update Claude account:', error);
+ throw error;
+ }
+ }
+
+ // 🗑️ 删除Claude账户
+ async deleteAccount(accountId) {
+ try {
+ const result = await redis.deleteClaudeAccount(accountId);
+
+ if (result === 0) {
+ throw new Error('Account not found');
+ }
+
+ logger.success(`🗑️ Deleted Claude account: ${accountId}`);
+
+ return { success: true };
+ } catch (error) {
+ logger.error('❌ Failed to delete Claude account:', error);
+ throw error;
+ }
+ }
+
+ // 🎯 智能选择可用账户
+ async selectAvailableAccount() {
+ try {
+ const accounts = await redis.getAllClaudeAccounts();
+
+ const activeAccounts = accounts.filter(account =>
+ account.isActive === 'true' &&
+ account.status !== 'error'
+ );
+
+ if (activeAccounts.length === 0) {
+ throw new Error('No active Claude accounts available');
+ }
+
+ // 优先选择最近刷新过token的账户
+ const sortedAccounts = activeAccounts.sort((a, b) => {
+ const aLastRefresh = new Date(a.lastRefreshAt || 0).getTime();
+ const bLastRefresh = new Date(b.lastRefreshAt || 0).getTime();
+ return bLastRefresh - aLastRefresh;
+ });
+
+ return sortedAccounts[0].id;
+ } catch (error) {
+ logger.error('❌ Failed to select available account:', error);
+ throw error;
+ }
+ }
+
+ // 🌐 创建代理agent
+ _createProxyAgent(proxyConfig) {
+ if (!proxyConfig) {
+ return null;
+ }
+
+ try {
+ const proxy = JSON.parse(proxyConfig);
+
+ if (proxy.type === 'socks5') {
+ const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '';
+ const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}`;
+ return new SocksProxyAgent(socksUrl);
+ } else if (proxy.type === 'http' || proxy.type === 'https') {
+ const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '';
+ const httpUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}`;
+ return new HttpsProxyAgent(httpUrl);
+ }
+ } catch (error) {
+ logger.warn('⚠️ Invalid proxy configuration:', error);
+ }
+
+ return null;
+ }
+
+ // 🔐 加密敏感数据
+ _encryptSensitiveData(data) {
+ if (!data) return '';
+
+ try {
+ const key = this._generateEncryptionKey();
+ const iv = crypto.randomBytes(16);
+
+ const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
+ let encrypted = cipher.update(data, 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ // 将IV和加密数据一起返回,用:分隔
+ return iv.toString('hex') + ':' + encrypted;
+ } catch (error) {
+ logger.error('❌ Encryption error:', error);
+ return data;
+ }
+ }
+
+ // 🔓 解密敏感数据
+ _decryptSensitiveData(encryptedData) {
+ if (!encryptedData) return '';
+
+ try {
+ // 检查是否是新格式(包含IV)
+ if (encryptedData.includes(':')) {
+ // 新格式:iv:encryptedData
+ const parts = encryptedData.split(':');
+ if (parts.length === 2) {
+ const key = this._generateEncryptionKey();
+ const iv = Buffer.from(parts[0], 'hex');
+ const encrypted = parts[1];
+
+ const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+ return decrypted;
+ }
+ }
+
+ // 旧格式或格式错误,尝试旧方式解密(向后兼容)
+ // 注意:在新版本Node.js中这将失败,但我们会捕获错误
+ try {
+ const decipher = crypto.createDecipher('aes-256-cbc', config.security.encryptionKey);
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+ return decrypted;
+ } catch (oldError) {
+ // 如果旧方式也失败,返回原数据
+ logger.warn('⚠️ Could not decrypt data, returning as-is:', oldError.message);
+ return encryptedData;
+ }
+ } catch (error) {
+ logger.error('❌ Decryption error:', error);
+ return encryptedData;
+ }
+ }
+
+ // 🔑 生成加密密钥(辅助方法)
+ _generateEncryptionKey() {
+ return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32);
+ }
+
+ // 🎭 掩码邮箱地址
+ _maskEmail(email) {
+ if (!email || !email.includes('@')) return email;
+
+ const [username, domain] = email.split('@');
+ const maskedUsername = username.length > 2
+ ? `${username.slice(0, 2)}***${username.slice(-1)}`
+ : `${username.slice(0, 1)}***`;
+
+ return `${maskedUsername}@${domain}`;
+ }
+
+ // 🧹 清理错误账户
+ async cleanupErrorAccounts() {
+ try {
+ const accounts = await redis.getAllClaudeAccounts();
+ let cleanedCount = 0;
+
+ for (const account of accounts) {
+ if (account.status === 'error' && account.lastRefreshAt) {
+ const lastRefresh = new Date(account.lastRefreshAt);
+ const now = new Date();
+ const hoursSinceLastRefresh = (now - lastRefresh) / (1000 * 60 * 60);
+
+ // 如果错误状态超过24小时,尝试重新激活
+ if (hoursSinceLastRefresh > 24) {
+ account.status = 'created';
+ account.errorMessage = '';
+ await redis.setClaudeAccount(account.id, account);
+ cleanedCount++;
+ }
+ }
+ }
+
+ if (cleanedCount > 0) {
+ logger.success(`🧹 Reset ${cleanedCount} error accounts`);
+ }
+
+ return cleanedCount;
+ } catch (error) {
+ logger.error('❌ Failed to cleanup error accounts:', error);
+ return 0;
+ }
+ }
+}
+
+module.exports = new ClaudeAccountService();
\ No newline at end of file
diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js
new file mode 100644
index 00000000..4e12530e
--- /dev/null
+++ b/src/services/claudeRelayService.js
@@ -0,0 +1,526 @@
+const https = require('https');
+const { SocksProxyAgent } = require('socks-proxy-agent');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const claudeAccountService = require('./claudeAccountService');
+const logger = require('../utils/logger');
+const config = require('../../config/config');
+
+class ClaudeRelayService {
+ constructor() {
+ this.claudeApiUrl = config.claude.apiUrl;
+ this.apiVersion = config.claude.apiVersion;
+ this.betaHeader = config.claude.betaHeader;
+ this.systemPrompt = config.claude.systemPrompt;
+ }
+
+ // 🚀 转发请求到Claude API
+ async relayRequest(requestBody, apiKeyData) {
+ try {
+ // 选择可用的Claude账户
+ const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount();
+
+ logger.info(`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}`);
+
+ // 获取有效的访问token
+ const accessToken = await claudeAccountService.getValidAccessToken(accountId);
+
+ // 处理请求体
+ const processedBody = this._processRequestBody(requestBody);
+
+ // 获取代理配置
+ const proxyAgent = await this._getProxyAgent(accountId);
+
+ // 发送请求到Claude API
+ const response = await this._makeClaudeRequest(processedBody, accessToken, proxyAgent);
+
+ // 记录成功的API调用
+ const inputTokens = requestBody.messages ?
+ requestBody.messages.reduce((sum, msg) => sum + (msg.content?.length || 0), 0) / 4 : 0; // 粗略估算
+ const outputTokens = response.content ?
+ response.content.reduce((sum, content) => sum + (content.text?.length || 0), 0) / 4 : 0;
+
+ logger.info(`✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens, Output: ~${Math.round(outputTokens)} tokens`);
+
+ return response;
+ } catch (error) {
+ logger.error(`❌ Claude relay request failed for key: ${apiKeyData.name || apiKeyData.id}:`, error.message);
+ throw error;
+ }
+ }
+
+ // 🔄 处理请求体
+ _processRequestBody(body) {
+ if (!body) return body;
+
+ // 深拷贝请求体
+ const processedBody = JSON.parse(JSON.stringify(body));
+
+ // 移除cache_control中的ttl字段
+ this._stripTtlFromCacheControl(processedBody);
+
+ // 只有在配置了系统提示时才添加
+ if (this.systemPrompt && this.systemPrompt.trim()) {
+ const systemPrompt = {
+ type: 'text',
+ text: this.systemPrompt
+ };
+
+ if (processedBody.system) {
+ if (Array.isArray(processedBody.system)) {
+ // 如果system数组存在但为空,或者没有有效内容,则添加系统提示
+ const hasValidContent = processedBody.system.some(item =>
+ item && item.text && item.text.trim()
+ );
+ if (!hasValidContent) {
+ processedBody.system = [systemPrompt];
+ } else {
+ processedBody.system.unshift(systemPrompt);
+ }
+ } else {
+ throw new Error('system field must be an array');
+ }
+ } else {
+ processedBody.system = [systemPrompt];
+ }
+ } else {
+ // 如果没有配置系统提示,且system字段为空,则删除它
+ if (processedBody.system && Array.isArray(processedBody.system)) {
+ const hasValidContent = processedBody.system.some(item =>
+ item && item.text && item.text.trim()
+ );
+ if (!hasValidContent) {
+ delete processedBody.system;
+ }
+ }
+ }
+
+ return processedBody;
+ }
+
+ // 🧹 移除TTL字段
+ _stripTtlFromCacheControl(body) {
+ if (!body || typeof body !== 'object') return;
+
+ const processContentArray = (contentArray) => {
+ if (!Array.isArray(contentArray)) return;
+
+ contentArray.forEach(item => {
+ if (item && typeof item === 'object' && item.cache_control) {
+ if (item.cache_control.ttl) {
+ delete item.cache_control.ttl;
+ logger.debug('🧹 Removed ttl from cache_control');
+ }
+ }
+ });
+ };
+
+ if (Array.isArray(body.system)) {
+ processContentArray(body.system);
+ }
+
+ if (Array.isArray(body.messages)) {
+ body.messages.forEach(message => {
+ if (message && Array.isArray(message.content)) {
+ processContentArray(message.content);
+ }
+ });
+ }
+ }
+
+ // 🌐 获取代理Agent
+ async _getProxyAgent(accountId) {
+ try {
+ const accountData = await claudeAccountService.getAllAccounts();
+ const account = accountData.find(acc => acc.id === accountId);
+
+ if (!account || !account.proxy) {
+ return null;
+ }
+
+ const proxy = account.proxy;
+
+ if (proxy.type === 'socks5') {
+ const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '';
+ const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}`;
+ return new SocksProxyAgent(socksUrl);
+ } else if (proxy.type === 'http' || proxy.type === 'https') {
+ const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '';
+ const httpUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}`;
+ return new HttpsProxyAgent(httpUrl);
+ }
+ } catch (error) {
+ logger.warn('⚠️ Failed to create proxy agent:', error);
+ }
+
+ return null;
+ }
+
+ // 🔗 发送请求到Claude API
+ async _makeClaudeRequest(body, accessToken, proxyAgent) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(this.claudeApiUrl);
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || 443,
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`,
+ 'anthropic-version': this.apiVersion,
+ 'User-Agent': 'claude-relay-service/1.0.0'
+ },
+ agent: proxyAgent,
+ timeout: config.proxy.timeout
+ };
+
+ if (this.betaHeader) {
+ options.headers['anthropic-beta'] = this.betaHeader;
+ }
+
+ const req = https.request(options, (res) => {
+ let responseData = '';
+
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ res.on('end', () => {
+ try {
+ const response = {
+ statusCode: res.statusCode,
+ headers: res.headers,
+ body: responseData
+ };
+
+ logger.debug(`🔗 Claude API response: ${res.statusCode}`);
+
+ resolve(response);
+ } catch (error) {
+ logger.error('❌ Failed to parse Claude API response:', error);
+ reject(error);
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ logger.error('❌ Claude API request error:', error);
+ reject(error);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ logger.error('❌ Claude API request timeout');
+ reject(new Error('Request timeout'));
+ });
+
+ // 写入请求体
+ req.write(JSON.stringify(body));
+ req.end();
+ });
+ }
+
+ // 🌊 处理流式响应(带usage数据捕获)
+ async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, usageCallback) {
+ try {
+ // 选择可用的Claude账户
+ const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount();
+
+ logger.info(`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}`);
+
+ // 获取有效的访问token
+ const accessToken = await claudeAccountService.getValidAccessToken(accountId);
+
+ // 处理请求体
+ const processedBody = this._processRequestBody(requestBody);
+
+ // 获取代理配置
+ const proxyAgent = await this._getProxyAgent(accountId);
+
+ // 发送流式请求并捕获usage数据
+ return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, responseStream, usageCallback);
+ } catch (error) {
+ logger.error('❌ Claude stream relay with usage capture failed:', error);
+ throw error;
+ }
+ }
+
+ // 🌊 发送流式请求到Claude API(带usage数据捕获)
+ async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, responseStream, usageCallback) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(this.claudeApiUrl);
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || 443,
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`,
+ 'anthropic-version': this.apiVersion,
+ 'User-Agent': 'claude-relay-service/1.0.0'
+ },
+ agent: proxyAgent,
+ timeout: config.proxy.timeout
+ };
+
+ if (this.betaHeader) {
+ options.headers['anthropic-beta'] = this.betaHeader;
+ }
+
+ const req = https.request(options, (res) => {
+ // 设置响应头
+ responseStream.statusCode = res.statusCode;
+ Object.keys(res.headers).forEach(key => {
+ responseStream.setHeader(key, res.headers[key]);
+ });
+
+ let buffer = '';
+ let finalUsageReported = false; // 防止重复统计的标志
+ let collectedUsageData = {}; // 收集来自不同事件的usage数据
+
+ // 监听数据块,解析SSE并寻找usage信息
+ res.on('data', (chunk) => {
+ const chunkStr = chunk.toString();
+
+ // 记录原始SSE数据块
+ logger.info('📡 Raw SSE chunk received:', {
+ length: chunkStr.length,
+ content: chunkStr
+ });
+
+ buffer += chunkStr;
+
+ // 处理完整的SSE行
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || ''; // 保留最后的不完整行
+
+ // 转发已处理的完整行到客户端
+ if (lines.length > 0) {
+ const linesToForward = lines.join('\n') + (lines.length > 0 ? '\n' : '');
+ responseStream.write(linesToForward);
+ }
+
+ for (const line of lines) {
+ // 记录每个SSE行
+ if (line.trim()) {
+ logger.info('📄 SSE Line:', line);
+ }
+
+ // 解析SSE数据寻找usage信息
+ if (line.startsWith('data: ') && line.length > 6) {
+ try {
+ const jsonStr = line.slice(6);
+ const data = JSON.parse(jsonStr);
+
+ // 收集来自不同事件的usage数据
+ if (data.type === 'message_start' && data.message && data.message.usage) {
+ // message_start包含input tokens、cache tokens和模型信息
+ collectedUsageData.input_tokens = data.message.usage.input_tokens || 0;
+ collectedUsageData.cache_creation_input_tokens = data.message.usage.cache_creation_input_tokens || 0;
+ collectedUsageData.cache_read_input_tokens = data.message.usage.cache_read_input_tokens || 0;
+ collectedUsageData.model = data.message.model;
+
+ logger.info('📊 Collected input/cache data from message_start:', JSON.stringify(collectedUsageData));
+ }
+
+ // message_delta包含最终的output tokens
+ if (data.type === 'message_delta' && data.usage && data.usage.output_tokens !== undefined) {
+ collectedUsageData.output_tokens = data.usage.output_tokens || 0;
+
+ logger.info('📊 Collected output data from message_delta:', JSON.stringify(collectedUsageData));
+
+ // 如果已经收集到了input数据,现在有了output数据,可以统计了
+ if (collectedUsageData.input_tokens !== undefined && !finalUsageReported) {
+ logger.info('🎯 Complete usage data collected, triggering callback');
+ usageCallback(collectedUsageData);
+ finalUsageReported = true;
+ }
+ }
+
+ } catch (parseError) {
+ // 忽略JSON解析错误,继续处理
+ logger.debug('🔍 SSE line not JSON or no usage data:', line.slice(0, 100));
+ }
+ }
+ }
+ });
+
+ res.on('end', () => {
+ // 处理缓冲区中剩余的数据
+ if (buffer.trim()) {
+ responseStream.write(buffer);
+ }
+ responseStream.end();
+
+ // 检查是否捕获到usage数据
+ if (!finalUsageReported) {
+ logger.warn('⚠️ Stream completed but no usage data was captured! This indicates a problem with SSE parsing or Claude API response format.');
+ }
+
+ logger.debug('🌊 Claude stream response with usage capture completed');
+ resolve();
+ });
+ });
+
+ req.on('error', (error) => {
+ logger.error('❌ Claude stream request error:', error);
+ if (!responseStream.headersSent) {
+ responseStream.writeHead(500, { 'Content-Type': 'application/json' });
+ }
+ if (!responseStream.destroyed) {
+ responseStream.end(JSON.stringify({ error: 'Upstream request failed' }));
+ }
+ reject(error);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ logger.error('❌ Claude stream request timeout');
+ if (!responseStream.headersSent) {
+ responseStream.writeHead(504, { 'Content-Type': 'application/json' });
+ }
+ if (!responseStream.destroyed) {
+ responseStream.end(JSON.stringify({ error: 'Request timeout' }));
+ }
+ reject(new Error('Request timeout'));
+ });
+
+ // 处理客户端断开连接
+ responseStream.on('close', () => {
+ logger.debug('🔌 Client disconnected, cleaning up stream');
+ if (!req.destroyed) {
+ req.destroy();
+ }
+ });
+
+ // 写入请求体
+ req.write(JSON.stringify(body));
+ req.end();
+ });
+ }
+
+ // 🌊 发送流式请求到Claude API
+ async _makeClaudeStreamRequest(body, accessToken, proxyAgent, responseStream) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(this.claudeApiUrl);
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || 443,
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`,
+ 'anthropic-version': this.apiVersion,
+ 'User-Agent': 'claude-relay-service/1.0.0'
+ },
+ agent: proxyAgent,
+ timeout: config.proxy.timeout
+ };
+
+ if (this.betaHeader) {
+ options.headers['anthropic-beta'] = this.betaHeader;
+ }
+
+ const req = https.request(options, (res) => {
+ // 设置响应头
+ responseStream.statusCode = res.statusCode;
+ Object.keys(res.headers).forEach(key => {
+ responseStream.setHeader(key, res.headers[key]);
+ });
+
+ // 管道响应数据
+ res.pipe(responseStream);
+
+ res.on('end', () => {
+ logger.debug('🌊 Claude stream response completed');
+ resolve();
+ });
+ });
+
+ req.on('error', (error) => {
+ logger.error('❌ Claude stream request error:', error);
+ if (!responseStream.headersSent) {
+ responseStream.writeHead(500, { 'Content-Type': 'application/json' });
+ }
+ if (!responseStream.destroyed) {
+ responseStream.end(JSON.stringify({ error: 'Upstream request failed' }));
+ }
+ reject(error);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ logger.error('❌ Claude stream request timeout');
+ if (!responseStream.headersSent) {
+ responseStream.writeHead(504, { 'Content-Type': 'application/json' });
+ }
+ if (!responseStream.destroyed) {
+ responseStream.end(JSON.stringify({ error: 'Request timeout' }));
+ }
+ reject(new Error('Request timeout'));
+ });
+
+ // 处理客户端断开连接
+ responseStream.on('close', () => {
+ logger.debug('🔌 Client disconnected, cleaning up stream');
+ if (!req.destroyed) {
+ req.destroy();
+ }
+ });
+
+ // 写入请求体
+ req.write(JSON.stringify(body));
+ req.end();
+ });
+ }
+
+ // 🔄 重试逻辑
+ async _retryRequest(requestFunc, maxRetries = 3) {
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ return await requestFunc();
+ } catch (error) {
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+ const delay = Math.pow(2, i) * 1000; // 指数退避
+ logger.warn(`⏳ Retry ${i + 1}/${maxRetries} in ${delay}ms: ${error.message}`);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+ }
+
+ throw lastError;
+ }
+
+ // 🎯 健康检查
+ async healthCheck() {
+ try {
+ const accounts = await claudeAccountService.getAllAccounts();
+ const activeAccounts = accounts.filter(acc => acc.isActive && acc.status === 'active');
+
+ return {
+ healthy: activeAccounts.length > 0,
+ activeAccounts: activeAccounts.length,
+ totalAccounts: accounts.length,
+ timestamp: new Date().toISOString()
+ };
+ } catch (error) {
+ logger.error('❌ Health check failed:', error);
+ return {
+ healthy: false,
+ error: error.message,
+ timestamp: new Date().toISOString()
+ };
+ }
+ }
+}
+
+module.exports = new ClaudeRelayService();
\ No newline at end of file
diff --git a/src/services/pricingService.js b/src/services/pricingService.js
new file mode 100644
index 00000000..4c135437
--- /dev/null
+++ b/src/services/pricingService.js
@@ -0,0 +1,234 @@
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const logger = require('../utils/logger');
+
+class PricingService {
+ constructor() {
+ this.dataDir = path.join(process.cwd(), 'data');
+ this.pricingFile = path.join(this.dataDir, 'model_pricing.json');
+ this.pricingUrl = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
+ this.pricingData = null;
+ this.lastUpdated = null;
+ this.updateInterval = 24 * 60 * 60 * 1000; // 24小时
+ }
+
+ // 初始化价格服务
+ async initialize() {
+ try {
+ // 确保data目录存在
+ if (!fs.existsSync(this.dataDir)) {
+ fs.mkdirSync(this.dataDir, { recursive: true });
+ logger.info('📁 Created data directory');
+ }
+
+ // 检查是否需要下载或更新价格数据
+ await this.checkAndUpdatePricing();
+
+ // 设置定时更新
+ setInterval(() => {
+ this.checkAndUpdatePricing();
+ }, this.updateInterval);
+
+ logger.success('💰 Pricing service initialized successfully');
+ } catch (error) {
+ logger.error('❌ Failed to initialize pricing service:', error);
+ }
+ }
+
+ // 检查并更新价格数据
+ async checkAndUpdatePricing() {
+ try {
+ const needsUpdate = this.needsUpdate();
+
+ if (needsUpdate) {
+ logger.info('🔄 Updating model pricing data...');
+ await this.downloadPricingData();
+ } else {
+ // 如果不需要更新,加载现有数据
+ await this.loadPricingData();
+ }
+ } catch (error) {
+ logger.error('❌ Failed to check/update pricing:', error);
+ // 如果更新失败,尝试加载现有数据
+ await this.loadPricingData();
+ }
+ }
+
+ // 检查是否需要更新
+ needsUpdate() {
+ if (!fs.existsSync(this.pricingFile)) {
+ logger.info('📋 Pricing file not found, will download');
+ return true;
+ }
+
+ const stats = fs.statSync(this.pricingFile);
+ const fileAge = Date.now() - stats.mtime.getTime();
+
+ if (fileAge > this.updateInterval) {
+ logger.info(`📋 Pricing file is ${Math.round(fileAge / (60 * 60 * 1000))} hours old, will update`);
+ return true;
+ }
+
+ return false;
+ }
+
+ // 下载价格数据
+ downloadPricingData() {
+ return new Promise((resolve, reject) => {
+ const request = https.get(this.pricingUrl, (response) => {
+ if (response.statusCode !== 200) {
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
+ return;
+ }
+
+ let data = '';
+ response.on('data', (chunk) => {
+ data += chunk;
+ });
+
+ response.on('end', () => {
+ try {
+ const jsonData = JSON.parse(data);
+
+ // 保存到文件
+ fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2));
+
+ // 更新内存中的数据
+ this.pricingData = jsonData;
+ this.lastUpdated = new Date();
+
+ logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`);
+ resolve();
+ } catch (error) {
+ reject(new Error(`Failed to parse pricing data: ${error.message}`));
+ }
+ });
+ });
+
+ request.on('error', (error) => {
+ reject(new Error(`Failed to download pricing data: ${error.message}`));
+ });
+
+ request.setTimeout(30000, () => {
+ request.destroy();
+ reject(new Error('Download timeout'));
+ });
+ });
+ }
+
+ // 加载本地价格数据
+ async loadPricingData() {
+ try {
+ if (fs.existsSync(this.pricingFile)) {
+ const data = fs.readFileSync(this.pricingFile, 'utf8');
+ this.pricingData = JSON.parse(data);
+
+ const stats = fs.statSync(this.pricingFile);
+ this.lastUpdated = stats.mtime;
+
+ logger.info(`💰 Loaded pricing data for ${Object.keys(this.pricingData).length} models from cache`);
+ } else {
+ logger.warn('💰 No pricing data file found');
+ this.pricingData = {};
+ }
+ } catch (error) {
+ logger.error('❌ Failed to load pricing data:', error);
+ this.pricingData = {};
+ }
+ }
+
+ // 获取模型价格信息
+ getModelPricing(modelName) {
+ if (!this.pricingData || !modelName) {
+ return null;
+ }
+
+ // 尝试直接匹配
+ if (this.pricingData[modelName]) {
+ return this.pricingData[modelName];
+ }
+
+ // 尝试模糊匹配(处理版本号等变化)
+ const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, '');
+
+ for (const [key, value] of Object.entries(this.pricingData)) {
+ const normalizedKey = key.toLowerCase().replace(/[_-]/g, '');
+ if (normalizedKey.includes(normalizedModel) || normalizedModel.includes(normalizedKey)) {
+ logger.debug(`💰 Found pricing for ${modelName} using fuzzy match: ${key}`);
+ return value;
+ }
+ }
+
+ logger.debug(`💰 No pricing found for model: ${modelName}`);
+ return null;
+ }
+
+ // 计算使用费用
+ calculateCost(usage, modelName) {
+ const pricing = this.getModelPricing(modelName);
+
+ if (!pricing) {
+ return {
+ inputCost: 0,
+ outputCost: 0,
+ cacheCreateCost: 0,
+ cacheReadCost: 0,
+ totalCost: 0,
+ hasPricing: false
+ };
+ }
+
+ const inputCost = (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0);
+ const outputCost = (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0);
+ const cacheCreateCost = (usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0);
+ const cacheReadCost = (usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0);
+
+ return {
+ inputCost,
+ outputCost,
+ cacheCreateCost,
+ cacheReadCost,
+ totalCost: inputCost + outputCost + cacheCreateCost + cacheReadCost,
+ hasPricing: true,
+ pricing: {
+ input: pricing.input_cost_per_token || 0,
+ output: pricing.output_cost_per_token || 0,
+ cacheCreate: pricing.cache_creation_input_token_cost || 0,
+ cacheRead: pricing.cache_read_input_token_cost || 0
+ }
+ };
+ }
+
+ // 格式化价格显示
+ formatCost(cost) {
+ if (cost === 0) return '$0.000000';
+ if (cost < 0.000001) return `$${cost.toExponential(2)}`;
+ if (cost < 0.01) return `$${cost.toFixed(6)}`;
+ if (cost < 1) return `$${cost.toFixed(4)}`;
+ return `$${cost.toFixed(2)}`;
+ }
+
+ // 获取服务状态
+ getStatus() {
+ return {
+ initialized: this.pricingData !== null,
+ lastUpdated: this.lastUpdated,
+ modelCount: this.pricingData ? Object.keys(this.pricingData).length : 0,
+ nextUpdate: this.lastUpdated ? new Date(this.lastUpdated.getTime() + this.updateInterval) : null
+ };
+ }
+
+ // 强制更新价格数据
+ async forceUpdate() {
+ try {
+ await this.downloadPricingData();
+ return { success: true, message: 'Pricing data updated successfully' };
+ } catch (error) {
+ logger.error('❌ Force update failed:', error);
+ return { success: false, message: error.message };
+ }
+ }
+}
+
+module.exports = new PricingService();
\ No newline at end of file
diff --git a/src/utils/costCalculator.js b/src/utils/costCalculator.js
new file mode 100644
index 00000000..656b9997
--- /dev/null
+++ b/src/utils/costCalculator.js
@@ -0,0 +1,224 @@
+const pricingService = require('../services/pricingService');
+
+// Claude模型价格配置 (USD per 1M tokens) - 备用定价
+const MODEL_PRICING = {
+ // Claude 3.5 Sonnet
+ 'claude-3-5-sonnet-20241022': {
+ input: 3.00,
+ output: 15.00,
+ cacheWrite: 3.75,
+ cacheRead: 0.30
+ },
+ 'claude-sonnet-4-20250514': {
+ input: 3.00,
+ output: 15.00,
+ cacheWrite: 3.75,
+ cacheRead: 0.30
+ },
+
+ // Claude 3.5 Haiku
+ 'claude-3-5-haiku-20241022': {
+ input: 0.25,
+ output: 1.25,
+ cacheWrite: 0.30,
+ cacheRead: 0.03
+ },
+
+ // Claude 3 Opus
+ 'claude-3-opus-20240229': {
+ input: 15.00,
+ output: 75.00,
+ cacheWrite: 18.75,
+ cacheRead: 1.50
+ },
+
+ // Claude 3 Sonnet
+ 'claude-3-sonnet-20240229': {
+ input: 3.00,
+ output: 15.00,
+ cacheWrite: 3.75,
+ cacheRead: 0.30
+ },
+
+ // Claude 3 Haiku
+ 'claude-3-haiku-20240307': {
+ input: 0.25,
+ output: 1.25,
+ cacheWrite: 0.30,
+ cacheRead: 0.03
+ },
+
+ // 默认定价(用于未知模型)
+ 'unknown': {
+ input: 3.00,
+ output: 15.00,
+ cacheWrite: 3.75,
+ cacheRead: 0.30
+ }
+};
+
+class CostCalculator {
+
+ /**
+ * 计算单次请求的费用
+ * @param {Object} usage - 使用量数据
+ * @param {number} usage.input_tokens - 输入token数量
+ * @param {number} usage.output_tokens - 输出token数量
+ * @param {number} usage.cache_creation_input_tokens - 缓存创建token数量
+ * @param {number} usage.cache_read_input_tokens - 缓存读取token数量
+ * @param {string} model - 模型名称
+ * @returns {Object} 费用详情
+ */
+ static calculateCost(usage, model = 'unknown') {
+ const inputTokens = usage.input_tokens || 0;
+ const outputTokens = usage.output_tokens || 0;
+ const cacheCreateTokens = usage.cache_creation_input_tokens || 0;
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
+
+ // 优先使用动态价格服务
+ const pricingData = pricingService.getModelPricing(model);
+ let pricing;
+ let usingDynamicPricing = false;
+
+ if (pricingData) {
+ // 转换动态价格格式为内部格式
+ pricing = {
+ input: (pricingData.input_cost_per_token || 0) * 1000000, // 转换为per 1M tokens
+ output: (pricingData.output_cost_per_token || 0) * 1000000,
+ cacheWrite: (pricingData.cache_creation_input_token_cost || 0) * 1000000,
+ cacheRead: (pricingData.cache_read_input_token_cost || 0) * 1000000
+ };
+ usingDynamicPricing = true;
+ } else {
+ // 回退到静态价格
+ pricing = MODEL_PRICING[model] || MODEL_PRICING['unknown'];
+ }
+
+ // 计算各类型token的费用 (USD)
+ const inputCost = (inputTokens / 1000000) * pricing.input;
+ const outputCost = (outputTokens / 1000000) * pricing.output;
+ const cacheWriteCost = (cacheCreateTokens / 1000000) * pricing.cacheWrite;
+ const cacheReadCost = (cacheReadTokens / 1000000) * pricing.cacheRead;
+
+ const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost;
+
+ return {
+ model,
+ pricing,
+ usingDynamicPricing,
+ usage: {
+ inputTokens,
+ outputTokens,
+ cacheCreateTokens,
+ cacheReadTokens,
+ totalTokens: inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
+ },
+ costs: {
+ input: inputCost,
+ output: outputCost,
+ cacheWrite: cacheWriteCost,
+ cacheRead: cacheReadCost,
+ total: totalCost
+ },
+ // 格式化的费用字符串
+ formatted: {
+ input: this.formatCost(inputCost),
+ output: this.formatCost(outputCost),
+ cacheWrite: this.formatCost(cacheWriteCost),
+ cacheRead: this.formatCost(cacheReadCost),
+ total: this.formatCost(totalCost)
+ }
+ };
+ }
+
+ /**
+ * 计算聚合使用量的费用
+ * @param {Object} aggregatedUsage - 聚合使用量数据
+ * @param {string} model - 模型名称
+ * @returns {Object} 费用详情
+ */
+ static calculateAggregatedCost(aggregatedUsage, model = 'unknown') {
+ const usage = {
+ input_tokens: aggregatedUsage.inputTokens || aggregatedUsage.totalInputTokens || 0,
+ output_tokens: aggregatedUsage.outputTokens || aggregatedUsage.totalOutputTokens || 0,
+ cache_creation_input_tokens: aggregatedUsage.cacheCreateTokens || aggregatedUsage.totalCacheCreateTokens || 0,
+ cache_read_input_tokens: aggregatedUsage.cacheReadTokens || aggregatedUsage.totalCacheReadTokens || 0
+ };
+
+ return this.calculateCost(usage, model);
+ }
+
+ /**
+ * 获取模型定价信息
+ * @param {string} model - 模型名称
+ * @returns {Object} 定价信息
+ */
+ static getModelPricing(model = 'unknown') {
+ return MODEL_PRICING[model] || MODEL_PRICING['unknown'];
+ }
+
+ /**
+ * 获取所有支持的模型和定价
+ * @returns {Object} 所有模型定价
+ */
+ static getAllModelPricing() {
+ return { ...MODEL_PRICING };
+ }
+
+ /**
+ * 验证模型是否支持
+ * @param {string} model - 模型名称
+ * @returns {boolean} 是否支持
+ */
+ static isModelSupported(model) {
+ return !!MODEL_PRICING[model];
+ }
+
+ /**
+ * 格式化费用显示
+ * @param {number} cost - 费用金额
+ * @param {number} decimals - 小数位数
+ * @returns {string} 格式化的费用字符串
+ */
+ static formatCost(cost, decimals = 6) {
+ if (cost >= 1) {
+ return `$${cost.toFixed(2)}`;
+ } else if (cost >= 0.001) {
+ return `$${cost.toFixed(4)}`;
+ } else {
+ return `$${cost.toFixed(decimals)}`;
+ }
+ }
+
+ /**
+ * 计算费用节省(使用缓存的节省)
+ * @param {Object} usage - 使用量数据
+ * @param {string} model - 模型名称
+ * @returns {Object} 节省信息
+ */
+ static calculateCacheSavings(usage, model = 'unknown') {
+ const pricing = this.getModelPricing(model);
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
+
+ // 如果这些token不使用缓存,需要按正常input价格计费
+ const normalCost = (cacheReadTokens / 1000000) * pricing.input;
+ const cacheCost = (cacheReadTokens / 1000000) * pricing.cacheRead;
+ const savings = normalCost - cacheCost;
+ const savingsPercentage = normalCost > 0 ? (savings / normalCost) * 100 : 0;
+
+ return {
+ normalCost,
+ cacheCost,
+ savings,
+ savingsPercentage,
+ formatted: {
+ normalCost: this.formatCost(normalCost),
+ cacheCost: this.formatCost(cacheCost),
+ savings: this.formatCost(savings),
+ savingsPercentage: `${savingsPercentage.toFixed(1)}%`
+ }
+ };
+ }
+}
+
+module.exports = CostCalculator;
\ No newline at end of file
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 00000000..9f42879f
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,290 @@
+const winston = require('winston');
+const DailyRotateFile = require('winston-daily-rotate-file');
+const config = require('../../config/config');
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+
+// 📝 增强的日志格式
+const createLogFormat = (colorize = false) => {
+ const formats = [
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
+ winston.format.errors({ stack: true }),
+ winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'stack'] })
+ ];
+
+ if (colorize) {
+ formats.push(winston.format.colorize());
+ }
+
+ formats.push(
+ winston.format.printf(({ level, message, timestamp, stack, metadata, ...rest }) => {
+ const emoji = {
+ error: '❌',
+ warn: '⚠️ ',
+ info: 'ℹ️ ',
+ debug: '🐛',
+ verbose: '📝'
+ };
+
+ let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`;
+
+ // 添加元数据
+ if (metadata && Object.keys(metadata).length > 0) {
+ logMessage += ` | ${JSON.stringify(metadata)}`;
+ }
+
+ // 添加其他属性
+ const additionalData = { ...rest };
+ delete additionalData.level;
+ delete additionalData.message;
+ delete additionalData.timestamp;
+ delete additionalData.stack;
+
+ if (Object.keys(additionalData).length > 0) {
+ logMessage += ` | ${JSON.stringify(additionalData)}`;
+ }
+
+ return stack ? `${logMessage}\n${stack}` : logMessage;
+ })
+ );
+
+ return winston.format.combine(...formats);
+};
+
+const logFormat = createLogFormat(false);
+const consoleFormat = createLogFormat(true);
+
+// 📁 确保日志目录存在并设置权限
+if (!fs.existsSync(config.logging.dirname)) {
+ fs.mkdirSync(config.logging.dirname, { recursive: true, mode: 0o755 });
+}
+
+// 🔄 增强的日志轮转配置
+const createRotateTransport = (filename, level = null) => {
+ const transport = new DailyRotateFile({
+ filename: path.join(config.logging.dirname, filename),
+ datePattern: 'YYYY-MM-DD',
+ zippedArchive: true,
+ maxSize: config.logging.maxSize,
+ maxFiles: config.logging.maxFiles,
+ auditFile: path.join(config.logging.dirname, `.${filename.replace('%DATE%', 'audit')}.json`),
+ format: logFormat
+ });
+
+ if (level) {
+ transport.level = level;
+ }
+
+ // 监听轮转事件
+ transport.on('rotate', (oldFilename, newFilename) => {
+ console.log(`📦 Log rotated: ${oldFilename} -> ${newFilename}`);
+ });
+
+ transport.on('new', (newFilename) => {
+ console.log(`📄 New log file created: ${newFilename}`);
+ });
+
+ transport.on('archive', (zipFilename) => {
+ console.log(`🗜️ Log archived: ${zipFilename}`);
+ });
+
+ return transport;
+};
+
+const dailyRotateFileTransport = createRotateTransport('claude-relay-%DATE%.log');
+const errorFileTransport = createRotateTransport('claude-relay-error-%DATE%.log', 'error');
+
+// 🔒 创建专门的安全日志记录器
+const securityLogger = winston.createLogger({
+ level: 'warn',
+ format: logFormat,
+ transports: [
+ createRotateTransport('claude-relay-security-%DATE%.log', 'warn')
+ ],
+ silent: false
+});
+
+// 🌟 增强的 Winston logger
+const logger = winston.createLogger({
+ level: config.logging.level,
+ format: logFormat,
+ transports: [
+ // 📄 文件输出
+ dailyRotateFileTransport,
+ errorFileTransport,
+
+ // 🖥️ 控制台输出
+ new winston.transports.Console({
+ format: consoleFormat,
+ handleExceptions: false,
+ handleRejections: false
+ })
+ ],
+
+ // 🚨 异常处理
+ exceptionHandlers: [
+ new winston.transports.File({
+ filename: path.join(config.logging.dirname, 'exceptions.log'),
+ format: logFormat,
+ maxsize: 10485760, // 10MB
+ maxFiles: 5
+ }),
+ new winston.transports.Console({
+ format: consoleFormat
+ })
+ ],
+
+ // 🔄 未捕获异常处理
+ rejectionHandlers: [
+ new winston.transports.File({
+ filename: path.join(config.logging.dirname, 'rejections.log'),
+ format: logFormat,
+ maxsize: 10485760, // 10MB
+ maxFiles: 5
+ }),
+ new winston.transports.Console({
+ format: consoleFormat
+ })
+ ],
+
+ // 防止进程退出
+ exitOnError: false
+});
+
+// 🎯 增强的自定义方法
+logger.success = (message, metadata = {}) => {
+ logger.info(`✅ ${message}`, { type: 'success', ...metadata });
+};
+
+logger.start = (message, metadata = {}) => {
+ logger.info(`🚀 ${message}`, { type: 'startup', ...metadata });
+};
+
+logger.request = (method, url, status, duration, metadata = {}) => {
+ const emoji = status >= 400 ? '🔴' : status >= 300 ? '🟡' : '🟢';
+ const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info';
+
+ logger[level](`${emoji} ${method} ${url} - ${status} (${duration}ms)`, {
+ type: 'request',
+ method,
+ url,
+ status,
+ duration,
+ ...metadata
+ });
+};
+
+logger.api = (message, metadata = {}) => {
+ logger.info(`🔗 ${message}`, { type: 'api', ...metadata });
+};
+
+logger.security = (message, metadata = {}) => {
+ const securityData = {
+ type: 'security',
+ timestamp: new Date().toISOString(),
+ pid: process.pid,
+ hostname: os.hostname(),
+ ...metadata
+ };
+
+ // 记录到主日志
+ logger.warn(`🔒 ${message}`, securityData);
+
+ // 记录到专门的安全日志文件
+ try {
+ securityLogger.warn(`🔒 ${message}`, securityData);
+ } catch (error) {
+ // 如果安全日志文件不可用,只记录到主日志
+ console.warn('Security logger not available:', error.message);
+ }
+};
+
+logger.database = (message, metadata = {}) => {
+ logger.debug(`💾 ${message}`, { type: 'database', ...metadata });
+};
+
+logger.performance = (message, metadata = {}) => {
+ logger.info(`⚡ ${message}`, { type: 'performance', ...metadata });
+};
+
+logger.audit = (message, metadata = {}) => {
+ logger.info(`📋 ${message}`, {
+ type: 'audit',
+ timestamp: new Date().toISOString(),
+ pid: process.pid,
+ ...metadata
+ });
+};
+
+// 🔧 性能监控方法
+logger.timer = (label) => {
+ const start = Date.now();
+ return {
+ end: (message = '', metadata = {}) => {
+ const duration = Date.now() - start;
+ logger.performance(`${label} ${message}`, { duration, ...metadata });
+ return duration;
+ }
+ };
+};
+
+// 📊 日志统计
+logger.stats = {
+ requests: 0,
+ errors: 0,
+ warnings: 0
+};
+
+// 重写原始方法以统计
+const originalError = logger.error;
+const originalWarn = logger.warn;
+const originalInfo = logger.info;
+
+logger.error = function(message, metadata = {}) {
+ logger.stats.errors++;
+ return originalError.call(this, message, metadata);
+};
+
+logger.warn = function(message, metadata = {}) {
+ logger.stats.warnings++;
+ return originalWarn.call(this, message, metadata);
+};
+
+logger.info = function(message, metadata = {}) {
+ if (metadata.type === 'request') {
+ logger.stats.requests++;
+ }
+ return originalInfo.call(this, message, metadata);
+};
+
+// 📈 获取日志统计
+logger.getStats = () => ({ ...logger.stats });
+
+// 🧹 清理统计
+logger.resetStats = () => {
+ logger.stats.requests = 0;
+ logger.stats.errors = 0;
+ logger.stats.warnings = 0;
+};
+
+// 📡 健康检查
+logger.healthCheck = () => {
+ try {
+ const testMessage = 'Logger health check';
+ logger.debug(testMessage);
+ return { healthy: true, timestamp: new Date().toISOString() };
+ } catch (error) {
+ return { healthy: false, error: error.message, timestamp: new Date().toISOString() };
+ }
+};
+
+// 🎬 启动日志记录系统
+logger.start('Logger initialized', {
+ level: config.logging.level,
+ directory: config.logging.dirname,
+ maxSize: config.logging.maxSize,
+ maxFiles: config.logging.maxFiles
+});
+
+module.exports = logger;
\ No newline at end of file
diff --git a/src/utils/oauthHelper.js b/src/utils/oauthHelper.js
new file mode 100644
index 00000000..784d8abf
--- /dev/null
+++ b/src/utils/oauthHelper.js
@@ -0,0 +1,307 @@
+/**
+ * OAuth助手工具
+ * 基于claude-code-login.js中的OAuth流程实现
+ */
+
+const crypto = require('crypto');
+const { SocksProxyAgent } = require('socks-proxy-agent');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const axios = require('axios');
+const logger = require('./logger');
+
+// OAuth 配置常量 - 从claude-code-login.js提取
+const OAUTH_CONFIG = {
+ AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
+ TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
+ CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
+ REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
+ SCOPES: 'org:create_api_key user:profile user:inference'
+};
+
+/**
+ * 生成随机的 state 参数
+ * @returns {string} 随机生成的 state (64字符hex)
+ */
+function generateState() {
+ return crypto.randomBytes(32).toString('hex');
+}
+
+/**
+ * 生成随机的 code verifier(PKCE)
+ * @returns {string} base64url 编码的随机字符串
+ */
+function generateCodeVerifier() {
+ return crypto.randomBytes(32).toString('base64url');
+}
+
+/**
+ * 生成 code challenge(PKCE)
+ * @param {string} codeVerifier - code verifier 字符串
+ * @returns {string} SHA256 哈希后的 base64url 编码字符串
+ */
+function generateCodeChallenge(codeVerifier) {
+ return crypto.createHash('sha256')
+ .update(codeVerifier)
+ .digest('base64url');
+}
+
+/**
+ * 生成授权 URL
+ * @param {string} codeChallenge - PKCE code challenge
+ * @param {string} state - state 参数
+ * @returns {string} 完整的授权 URL
+ */
+function generateAuthUrl(codeChallenge, state) {
+ const params = new URLSearchParams({
+ code: 'true',
+ client_id: OAUTH_CONFIG.CLIENT_ID,
+ response_type: 'code',
+ redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
+ scope: OAUTH_CONFIG.SCOPES,
+ code_challenge: codeChallenge,
+ code_challenge_method: 'S256',
+ state: state
+ });
+
+ return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
+}
+
+/**
+ * 生成OAuth授权URL和相关参数
+ * @returns {{authUrl: string, codeVerifier: string, state: string, codeChallenge: string}}
+ */
+function generateOAuthParams() {
+ const state = generateState();
+ const codeVerifier = generateCodeVerifier();
+ const codeChallenge = generateCodeChallenge(codeVerifier);
+
+ const authUrl = generateAuthUrl(codeChallenge, state);
+
+ return {
+ authUrl,
+ codeVerifier,
+ state,
+ codeChallenge
+ };
+}
+
+/**
+ * 创建代理agent
+ * @param {object|null} proxyConfig - 代理配置对象
+ * @returns {object|null} 代理agent或null
+ */
+function createProxyAgent(proxyConfig) {
+ if (!proxyConfig) {
+ return null;
+ }
+
+ try {
+ if (proxyConfig.type === 'socks5') {
+ const auth = proxyConfig.username && proxyConfig.password ? `${proxyConfig.username}:${proxyConfig.password}@` : '';
+ const socksUrl = `socks5://${auth}${proxyConfig.host}:${proxyConfig.port}`;
+ return new SocksProxyAgent(socksUrl);
+ } else if (proxyConfig.type === 'http' || proxyConfig.type === 'https') {
+ const auth = proxyConfig.username && proxyConfig.password ? `${proxyConfig.username}:${proxyConfig.password}@` : '';
+ const httpUrl = `${proxyConfig.type}://${auth}${proxyConfig.host}:${proxyConfig.port}`;
+ return new HttpsProxyAgent(httpUrl);
+ }
+ } catch (error) {
+ console.warn('⚠️ Invalid proxy configuration:', error);
+ }
+
+ return null;
+}
+
+/**
+ * 使用授权码交换访问令牌
+ * @param {string} authorizationCode - 授权码
+ * @param {string} codeVerifier - PKCE code verifier
+ * @param {string} state - state 参数
+ * @param {object|null} proxyConfig - 代理配置(可选)
+ * @returns {Promise} Claude格式的token响应
+ */
+async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, proxyConfig = null) {
+ // 清理授权码,移除URL片段
+ const cleanedCode = authorizationCode.split('#')[0]?.split('&')[0] ?? authorizationCode;
+
+ const params = {
+ grant_type: 'authorization_code',
+ client_id: OAUTH_CONFIG.CLIENT_ID,
+ code: cleanedCode,
+ redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
+ code_verifier: codeVerifier,
+ state: state
+ };
+
+ // 创建代理agent
+ const agent = createProxyAgent(proxyConfig);
+
+ try {
+ logger.debug('🔄 Attempting OAuth token exchange', {
+ url: OAUTH_CONFIG.TOKEN_URL,
+ codeLength: cleanedCode.length,
+ codePrefix: cleanedCode.substring(0, 10) + '...',
+ hasProxy: !!proxyConfig,
+ proxyType: proxyConfig?.type || 'none'
+ });
+
+ const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
+ 'Accept': 'application/json, text/plain, */*',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ 'Referer': 'https://claude.ai/',
+ 'Origin': 'https://claude.ai'
+ },
+ httpsAgent: agent,
+ timeout: 30000
+ });
+
+ logger.success('✅ OAuth token exchange successful', {
+ status: response.status,
+ hasAccessToken: !!response.data?.access_token,
+ hasRefreshToken: !!response.data?.refresh_token,
+ scopes: response.data?.scope
+ });
+
+ const data = response.data;
+
+ // 返回Claude格式的token数据
+ return {
+ accessToken: data.access_token,
+ refreshToken: data.refresh_token,
+ expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000,
+ scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'],
+ isMax: true
+ };
+ } catch (error) {
+ // 处理axios错误响应
+ if (error.response) {
+ // 服务器返回了错误状态码
+ const status = error.response.status;
+ const errorData = error.response.data;
+
+ logger.error('❌ OAuth token exchange failed with server error', {
+ status: status,
+ statusText: error.response.statusText,
+ headers: error.response.headers,
+ data: errorData,
+ codeLength: cleanedCode.length,
+ codePrefix: cleanedCode.substring(0, 10) + '...'
+ });
+
+ // 尝试从错误响应中提取有用信息
+ let errorMessage = `HTTP ${status}`;
+
+ if (errorData) {
+ if (typeof errorData === 'string') {
+ errorMessage += `: ${errorData}`;
+ } else if (errorData.error) {
+ errorMessage += `: ${errorData.error}`;
+ if (errorData.error_description) {
+ errorMessage += ` - ${errorData.error_description}`;
+ }
+ } else {
+ errorMessage += `: ${JSON.stringify(errorData)}`;
+ }
+ }
+
+ throw new Error(`Token exchange failed: ${errorMessage}`);
+ } else if (error.request) {
+ // 请求被发送但没有收到响应
+ logger.error('❌ OAuth token exchange failed with network error', {
+ message: error.message,
+ code: error.code,
+ hasProxy: !!proxyConfig
+ });
+ throw new Error('Token exchange failed: No response from server (network error or timeout)');
+ } else {
+ // 其他错误
+ logger.error('❌ OAuth token exchange failed with unknown error', {
+ message: error.message,
+ stack: error.stack
+ });
+ throw new Error(`Token exchange failed: ${error.message}`);
+ }
+ }
+}
+
+/**
+ * 解析回调 URL 或授权码
+ * @param {string} input - 完整的回调 URL 或直接的授权码
+ * @returns {string} 授权码
+ */
+function parseCallbackUrl(input) {
+ if (!input || typeof input !== 'string') {
+ throw new Error('请提供有效的授权码或回调 URL');
+ }
+
+ const trimmedInput = input.trim();
+
+ // 情况1: 尝试作为完整URL解析
+ if (trimmedInput.startsWith('http://') || trimmedInput.startsWith('https://')) {
+ try {
+ const urlObj = new URL(trimmedInput);
+ const authorizationCode = urlObj.searchParams.get('code');
+
+ if (!authorizationCode) {
+ throw new Error('回调 URL 中未找到授权码 (code 参数)');
+ }
+
+ return authorizationCode;
+ } catch (error) {
+ if (error.message.includes('回调 URL 中未找到授权码')) {
+ throw error;
+ }
+ throw new Error('无效的 URL 格式,请检查回调 URL 是否正确');
+ }
+ }
+
+ // 情况2: 直接的授权码(可能包含URL fragments)
+ // 参考claude-code-login.js的处理方式:移除URL fragments和参数
+ const cleanedCode = trimmedInput.split('#')[0]?.split('&')[0] ?? trimmedInput;
+
+ // 验证授权码格式(Claude的授权码通常是base64url格式)
+ if (!cleanedCode || cleanedCode.length < 10) {
+ throw new Error('授权码格式无效,请确保复制了完整的 Authorization Code');
+ }
+
+ // 基本格式验证:授权码应该只包含字母、数字、下划线、连字符
+ const validCodePattern = /^[A-Za-z0-9_-]+$/;
+ if (!validCodePattern.test(cleanedCode)) {
+ throw new Error('授权码包含无效字符,请检查是否复制了正确的 Authorization Code');
+ }
+
+ return cleanedCode;
+}
+
+/**
+ * 格式化为Claude标准格式
+ * @param {object} tokenData - token数据
+ * @returns {object} claudeAiOauth格式的数据
+ */
+function formatClaudeCredentials(tokenData) {
+ return {
+ claudeAiOauth: {
+ accessToken: tokenData.accessToken,
+ refreshToken: tokenData.refreshToken,
+ expiresAt: tokenData.expiresAt,
+ scopes: tokenData.scopes,
+ isMax: tokenData.isMax
+ }
+ };
+}
+
+module.exports = {
+ OAUTH_CONFIG,
+ generateOAuthParams,
+ exchangeCodeForTokens,
+ parseCallbackUrl,
+ formatClaudeCredentials,
+ generateState,
+ generateCodeVerifier,
+ generateCodeChallenge,
+ generateAuthUrl,
+ createProxyAgent
+};
\ No newline at end of file
diff --git a/web/admin/app.js b/web/admin/app.js
new file mode 100644
index 00000000..10d986e0
--- /dev/null
+++ b/web/admin/app.js
@@ -0,0 +1,1989 @@
+const { createApp } = Vue;
+
+const app = createApp({
+ data() {
+ return {
+ isLoggedIn: false,
+ authToken: localStorage.getItem('authToken'),
+ activeTab: 'dashboard',
+
+ // Toast 通知
+ toasts: [],
+ toastIdCounter: 0,
+
+ // 登录相关
+ loginForm: {
+ username: '',
+ password: ''
+ },
+ loginLoading: false,
+ loginError: '',
+
+ // 标签页
+ tabs: [
+ { key: 'dashboard', name: '仪表板', icon: 'fas fa-tachometer-alt' },
+ { key: 'apiKeys', name: 'API Keys', icon: 'fas fa-key' },
+ { key: 'accounts', name: 'Claude账户', icon: 'fas fa-user-circle' },
+ { key: 'tutorial', name: '使用教程', icon: 'fas fa-graduation-cap' }
+ ],
+
+ // 教程系统选择
+ activeTutorialSystem: 'windows',
+ tutorialSystems: [
+ { key: 'windows', name: 'Windows', icon: 'fab fa-windows' },
+ { key: 'macos', name: 'macOS', icon: 'fab fa-apple' },
+ { key: 'linux', name: 'Linux / WSL2', icon: 'fab fa-ubuntu' }
+ ],
+
+ // 模型统计
+ modelStats: [],
+ modelStatsLoading: false,
+ modelStatsPeriod: 'daily',
+
+ // 数据
+ dashboardData: {
+ totalApiKeys: 0,
+ activeApiKeys: 0,
+ totalAccounts: 0,
+ activeAccounts: 0,
+ todayRequests: 0,
+ totalRequests: 0,
+ todayTokens: 0,
+ todayInputTokens: 0,
+ todayOutputTokens: 0,
+ totalTokens: 0,
+ totalInputTokens: 0,
+ totalOutputTokens: 0,
+ totalCacheCreateTokens: 0,
+ totalCacheReadTokens: 0,
+ todayCacheCreateTokens: 0,
+ todayCacheReadTokens: 0,
+ systemRPM: 0,
+ systemTPM: 0,
+ systemStatus: '正常',
+ uptime: 0
+ },
+
+ // 价格数据
+ costsData: {
+ todayCosts: { totalCost: 0, formatted: { totalCost: '$0.000000' } },
+ totalCosts: { totalCost: 0, formatted: { totalCost: '$0.000000' } }
+ },
+
+ // 仪表盘模型统计
+ dashboardModelStats: [],
+ dashboardModelPeriod: 'daily',
+ modelUsageChart: null,
+ usageTrendChart: null,
+ trendPeriod: 7,
+ trendData: [],
+
+ // 统一的日期筛选
+ dateFilter: {
+ type: 'preset', // preset 或 custom
+ preset: '7days', // today, 7days, 30days
+ customStart: '',
+ customEnd: '',
+ customRange: null, // Element Plus日期范围选择器的值
+ presetOptions: [
+ { value: 'today', label: '今天', days: 1 },
+ { value: '7days', label: '近7天', days: 7 },
+ { value: '30days', label: '近30天', days: 30 }
+ ]
+ },
+ showDateRangePicker: false, // 日期范围选择器显示状态
+ dateRangeInputValue: '', // 日期范围显示文本
+
+ // API Keys
+ apiKeys: [],
+ apiKeysLoading: false,
+ showCreateApiKeyModal: false,
+ createApiKeyLoading: false,
+ apiKeyForm: {
+ name: '',
+ tokenLimit: '',
+ description: ''
+ },
+ apiKeyModelStats: {}, // 存储每个key的模型统计数据
+ expandedApiKeys: {}, // 跟踪展开的API Keys
+ apiKeyModelPeriod: 'monthly', // API Key模型统计期间
+
+ // API Keys的日期筛选(每个API Key独立)
+ apiKeyDateFilters: {}, // 存储每个API Key的独立日期筛选状态
+ apiKeyDateFilterDefaults: {
+ type: 'preset', // preset 或 custom
+ preset: '7days', // today, 7days, 30days
+ customStart: '',
+ customEnd: '',
+ customRange: null, // Element Plus日期范围选择器的值
+ presetOptions: [
+ { value: 'today', label: '今天', days: 1 },
+ { value: '7days', label: '近7天', days: 7 },
+ { value: '30days', label: '近30天', days: 30 }
+ ]
+ },
+
+ // 新创建的API Key展示弹窗
+ showNewApiKeyModal: false,
+ newApiKey: {
+ key: '',
+ name: '',
+ description: '',
+ showFullKey: false
+ },
+
+ // 账户
+ accounts: [],
+ accountsLoading: false,
+ showCreateAccountModal: false,
+ createAccountLoading: false,
+ accountForm: {
+ name: '',
+ description: '',
+ proxyType: '',
+ proxyHost: '',
+ proxyPort: '',
+ proxyUsername: '',
+ proxyPassword: ''
+ },
+
+ // OAuth 相关
+ oauthStep: 1,
+ authUrlLoading: false,
+ oauthData: {
+ sessionId: '',
+ authUrl: '',
+ callbackUrl: ''
+ },
+
+ }
+ },
+
+ computed: {
+ // 动态计算BASE_URL
+ currentBaseUrl() {
+ return `${window.location.protocol}//${window.location.host}/api/`;
+ }
+ },
+
+ mounted() {
+ console.log('Vue app mounted, authToken:', !!this.authToken, 'activeTab:', this.activeTab);
+
+ // 初始化防抖函数
+ this.setTrendPeriod = this.debounce(this._setTrendPeriod, 300);
+
+ if (this.authToken) {
+ this.isLoggedIn = true;
+
+ // 初始化日期筛选器和图表数据
+ this.initializeDateFilter();
+
+ // 根据当前活跃标签页加载数据
+ this.loadCurrentTabData();
+ // 如果在仪表盘,等待Chart.js加载后初始化图表
+ if (this.activeTab === 'dashboard') {
+ this.waitForChartJS().then(() => {
+ this.loadDashboardModelStats();
+ this.loadUsageTrend();
+ });
+ }
+ } else {
+ console.log('No auth token found, user needs to login');
+ }
+ },
+
+ beforeUnmount() {
+ this.cleanupCharts();
+ },
+
+ watch: {
+ activeTab: {
+ handler(newTab, oldTab) {
+ console.log('Tab changed from:', oldTab, 'to:', newTab);
+
+ // 如果离开仪表盘标签页,清理图表
+ if (oldTab === 'dashboard' && newTab !== 'dashboard') {
+ this.cleanupCharts();
+ }
+
+ this.loadCurrentTabData();
+ },
+ immediate: false
+ }
+ },
+
+ methods: {
+ // Toast 通知方法
+ showToast(message, type = 'info', title = null, duration = 5000) {
+ const id = ++this.toastIdCounter;
+ const toast = {
+ id,
+ message,
+ type,
+ title,
+ show: false
+ };
+
+ this.toasts.push(toast);
+
+ // 延迟显示动画
+ setTimeout(() => {
+ const toastIndex = this.toasts.findIndex(t => t.id === id);
+ if (toastIndex !== -1) {
+ this.toasts[toastIndex].show = true;
+ }
+ }, 100);
+
+ // 自动移除
+ if (duration > 0) {
+ setTimeout(() => {
+ this.removeToast(id);
+ }, duration);
+ }
+ },
+
+ removeToast(id) {
+ const index = this.toasts.findIndex(t => t.id === id);
+ if (index !== -1) {
+ this.toasts[index].show = false;
+ setTimeout(() => {
+ const currentIndex = this.toasts.findIndex(t => t.id === id);
+ if (currentIndex !== -1) {
+ this.toasts.splice(currentIndex, 1);
+ }
+ }, 300);
+ }
+ },
+
+ getToastIcon(type) {
+ switch (type) {
+ case 'success': return 'fas fa-check-circle';
+ case 'error': return 'fas fa-exclamation-circle';
+ case 'warning': return 'fas fa-exclamation-triangle';
+ case 'info': return 'fas fa-info-circle';
+ default: return 'fas fa-info-circle';
+ }
+ },
+
+ // 打开创建API Key模态框
+ openCreateApiKeyModal() {
+ console.log('Opening API Key modal...');
+ // 先关闭所有其他模态框
+ this.showCreateAccountModal = false;
+ // 使用nextTick确保状态更新
+ this.$nextTick(() => {
+ this.showCreateApiKeyModal = true;
+ });
+ },
+
+ // 打开创建账户模态框
+ openCreateAccountModal() {
+ console.log('Opening Account modal...');
+ // 先关闭所有其他模态框
+ this.showCreateApiKeyModal = false;
+ // 使用nextTick确保状态更新
+ this.$nextTick(() => {
+ this.showCreateAccountModal = true;
+ this.resetAccountForm();
+ });
+ },
+
+ // 关闭创建账户模态框
+ closeCreateAccountModal() {
+ this.showCreateAccountModal = false;
+ this.resetAccountForm();
+ },
+
+ // 重置账户表单
+ resetAccountForm() {
+ this.accountForm = {
+ name: '',
+ description: '',
+ proxyType: '',
+ proxyHost: '',
+ proxyPort: '',
+ proxyUsername: '',
+ proxyPassword: ''
+ };
+ this.oauthStep = 1;
+ this.oauthData = {
+ sessionId: '',
+ authUrl: '',
+ callbackUrl: ''
+ };
+ },
+
+ // OAuth步骤前进
+ nextOAuthStep() {
+ if (this.oauthStep < 3) {
+ this.oauthStep++;
+ }
+ },
+
+ // 生成OAuth授权URL
+ async generateAuthUrl() {
+ this.authUrlLoading = true;
+ try {
+ // Build proxy configuration
+ let proxy = null;
+ if (this.accountForm.proxyType) {
+ proxy = {
+ type: this.accountForm.proxyType,
+ host: this.accountForm.proxyHost,
+ port: parseInt(this.accountForm.proxyPort),
+ username: this.accountForm.proxyUsername || null,
+ password: this.accountForm.proxyPassword || null
+ };
+ }
+
+ const response = await fetch('/admin/claude-accounts/generate-auth-url', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + this.authToken
+ },
+ body: JSON.stringify({
+ proxy: proxy
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.oauthData.authUrl = data.data.authUrl;
+ this.oauthData.sessionId = data.data.sessionId;
+ this.showToast('授权链接生成成功!', 'success', '生成成功');
+ } else {
+ this.showToast(data.message || '生成失败', 'error', '生成失败');
+ }
+ } catch (error) {
+ console.error('Error generating auth URL:', error);
+ this.showToast('生成失败,请检查网络连接', 'error', '网络错误');
+ } finally {
+ this.authUrlLoading = false;
+ }
+ },
+
+ // 复制到剪贴板
+ async copyToClipboard(text) {
+ try {
+ await navigator.clipboard.writeText(text);
+ this.showToast('已复制到剪贴板', 'success', '复制成功');
+ } catch (error) {
+ console.error('Copy failed:', error);
+ this.showToast('复制失败', 'error', '复制失败');
+ }
+ },
+
+ // 创建OAuth账户
+ async createOAuthAccount() {
+ this.createAccountLoading = true;
+ try {
+ // 首先交换authorization code获取token
+ const exchangeResponse = await fetch('/admin/claude-accounts/exchange-code', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + this.authToken
+ },
+ body: JSON.stringify({
+ sessionId: this.oauthData.sessionId,
+ callbackUrl: this.oauthData.callbackUrl
+ })
+ });
+
+ const exchangeData = await exchangeResponse.json();
+
+ if (!exchangeData.success) {
+ // Display detailed error information
+ const errorMsg = exchangeData.message || 'Token exchange failed';
+ this.showToast('Authorization failed: ' + errorMsg, 'error', 'Authorization Failed', 8000);
+ console.error('OAuth exchange failed:', exchangeData);
+ return;
+ }
+
+ // Build proxy configuration
+ let proxy = null;
+ if (this.accountForm.proxyType) {
+ proxy = {
+ type: this.accountForm.proxyType,
+ host: this.accountForm.proxyHost,
+ port: parseInt(this.accountForm.proxyPort),
+ username: this.accountForm.proxyUsername || null,
+ password: this.accountForm.proxyPassword || null
+ };
+ }
+
+ // 创建账户
+ const createResponse = await fetch('/admin/claude-accounts', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + this.authToken
+ },
+ body: JSON.stringify({
+ name: this.accountForm.name,
+ description: this.accountForm.description,
+ claudeAiOauth: exchangeData.data.claudeAiOauth,
+ proxy: proxy
+ })
+ });
+
+ const createData = await createResponse.json();
+
+ if (createData.success) {
+ this.showToast('OAuth账户创建成功!', 'success', '账户创建成功');
+ this.closeCreateAccountModal();
+ await this.loadAccounts();
+ } else {
+ this.showToast(createData.message || 'Account creation failed', 'error', 'Creation Failed');
+ }
+ } catch (error) {
+ console.error('Error creating OAuth account:', error);
+
+ // 尝试从错误响应中提取更详细的信息
+ let errorMessage = '创建失败,请检查网络连接';
+
+ if (error.response) {
+ try {
+ const errorData = await error.response.json();
+ errorMessage = errorData.message || errorMessage;
+ } catch (parseError) {
+ // 如果无法解析JSON,使用默认消息
+ console.error('Failed to parse error response:', parseError);
+ }
+ } else if (error.message) {
+ errorMessage = error.message;
+ }
+
+ this.showToast(errorMessage, 'error', '网络错误', 8000);
+ } finally {
+ this.createAccountLoading = false;
+ }
+ },
+
+
+ // 根据当前标签页加载数据
+ loadCurrentTabData() {
+ console.log('Loading current tab data for:', this.activeTab);
+ switch (this.activeTab) {
+ case 'dashboard':
+ this.loadDashboard();
+ // 加载图表数据,等待Chart.js
+ this.waitForChartJS().then(() => {
+ this.loadDashboardModelStats();
+ this.loadUsageTrend();
+ });
+ break;
+ case 'apiKeys':
+ this.loadApiKeys();
+ break;
+ case 'accounts':
+ this.loadAccounts();
+ break;
+ case 'models':
+ this.loadModelStats();
+ break;
+ case 'tutorial':
+ // 教程页面不需要加载数据
+ break;
+ }
+ },
+
+ // 等待Chart.js加载完成
+ waitForChartJS() {
+ return new Promise((resolve) => {
+ const checkChart = () => {
+ if (typeof Chart !== 'undefined') {
+ resolve();
+ } else {
+ setTimeout(checkChart, 100);
+ }
+ };
+ checkChart();
+ });
+ },
+
+ // 清理所有图表实例
+ cleanupCharts() {
+
+ // 清理模型使用图表
+ if (this.modelUsageChart) {
+ try {
+ // 先停止所有动画
+ this.modelUsageChart.stop();
+ // 再销毁图表
+ this.modelUsageChart.destroy();
+ } catch (error) {
+ console.warn('Error destroying model usage chart:', error);
+ }
+ this.modelUsageChart = null;
+ }
+
+ // 清理使用趋势图表
+ if (this.usageTrendChart) {
+ try {
+ // 先停止所有动画
+ this.usageTrendChart.stop();
+ // 再销毁图表
+ this.usageTrendChart.destroy();
+ } catch (error) {
+ console.warn('Error destroying usage trend chart:', error);
+ }
+ this.usageTrendChart = null;
+ }
+ },
+
+ // 检查DOM元素是否存在且有效
+ isElementValid(elementId) {
+ const element = document.getElementById(elementId);
+ return element && element.isConnected && element.ownerDocument && element.parentNode;
+ },
+
+ // 防抖函数,防止快速点击
+ debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ },
+
+ async login() {
+ this.loginLoading = true;
+ this.loginError = '';
+
+ try {
+ const response = await fetch('/web/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(this.loginForm)
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.authToken = data.token;
+ localStorage.setItem('authToken', this.authToken);
+ this.isLoggedIn = true;
+ this.loadDashboard();
+ } else {
+ this.loginError = data.message;
+ }
+ } catch (error) {
+ console.error('Login error:', error);
+ this.loginError = '登录失败,请检查网络连接';
+ } finally {
+ this.loginLoading = false;
+ }
+ },
+
+ async logout() {
+ if (this.authToken) {
+ try {
+ await fetch('/web/auth/logout', {
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Bearer ' + this.authToken
+ }
+ });
+ } catch (error) {
+ console.error('Logout error:', error);
+ }
+ }
+
+ this.authToken = null;
+ localStorage.removeItem('authToken');
+ this.isLoggedIn = false;
+ this.loginForm = { username: '', password: '' };
+ this.loginError = '';
+ },
+
+ async loadDashboard() {
+ try {
+ const [dashboardResponse, costsResponse] = await Promise.all([
+ fetch('/admin/dashboard', {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ }),
+ Promise.all([
+ fetch('/admin/usage-costs?period=today', {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ }),
+ fetch('/admin/usage-costs?period=all', {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ })
+ ])
+ ]);
+
+ const dashboardData = await dashboardResponse.json();
+ const [todayCostsResponse, totalCostsResponse] = costsResponse;
+ const todayCostsData = await todayCostsResponse.json();
+ const totalCostsData = await totalCostsResponse.json();
+
+ if (dashboardData.success) {
+ const overview = dashboardData.data.overview || {};
+ const recentActivity = dashboardData.data.recentActivity || {};
+ const systemAverages = dashboardData.data.systemAverages || {};
+ const systemHealth = dashboardData.data.systemHealth || {};
+
+ this.dashboardData = {
+ totalApiKeys: overview.totalApiKeys || 0,
+ activeApiKeys: overview.activeApiKeys || 0,
+ totalAccounts: overview.totalClaudeAccounts || 0,
+ activeAccounts: overview.activeClaudeAccounts || 0,
+ todayRequests: recentActivity.requestsToday || 0,
+ totalRequests: overview.totalRequestsUsed || 0,
+ todayTokens: recentActivity.tokensToday || 0,
+ todayInputTokens: recentActivity.inputTokensToday || 0,
+ todayOutputTokens: recentActivity.outputTokensToday || 0,
+ totalTokens: overview.totalTokensUsed || 0,
+ totalInputTokens: overview.totalInputTokensUsed || 0,
+ totalOutputTokens: overview.totalOutputTokensUsed || 0,
+ totalCacheCreateTokens: overview.totalCacheCreateTokensUsed || 0,
+ totalCacheReadTokens: overview.totalCacheReadTokensUsed || 0,
+ todayCacheCreateTokens: recentActivity.cacheCreateTokensToday || 0,
+ todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0,
+ systemRPM: systemAverages.rpm || 0,
+ systemTPM: systemAverages.tpm || 0,
+ systemStatus: systemHealth.redisConnected ? '正常' : '异常',
+ uptime: systemHealth.uptime || 0
+ };
+ }
+
+ // 更新费用数据
+ if (todayCostsData.success && totalCostsData.success) {
+ this.costsData = {
+ todayCosts: todayCostsData.data.totalCosts || { totalCost: 0, formatted: { totalCost: '$0.000000' } },
+ totalCosts: totalCostsData.data.totalCosts || { totalCost: 0, formatted: { totalCost: '$0.000000' } }
+ };
+ }
+ } catch (error) {
+ console.error('Failed to load dashboard:', error);
+ }
+ },
+
+ async loadApiKeys() {
+ this.apiKeysLoading = true;
+ console.log('Loading API Keys...');
+ try {
+ const response = await fetch('/admin/api-keys', {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+ const data = await response.json();
+
+ console.log('API Keys response:', data);
+
+ if (data.success) {
+ // 确保每个 API key 都有必要的属性
+ this.apiKeys = (data.data || []).map(key => {
+ const processedKey = {
+ ...key,
+ apiKey: key.apiKey || '',
+ name: key.name || 'Unknown',
+ id: key.id || '',
+ isActive: key.isActive !== undefined ? key.isActive : true,
+ usage: key.usage || { tokensUsed: 0 },
+ tokenLimit: key.tokenLimit || null,
+ createdAt: key.createdAt || new Date().toISOString()
+ };
+
+ // 为每个API Key初始化独立的日期筛选状态
+ if (!this.apiKeyDateFilters[processedKey.id]) {
+ this.initApiKeyDateFilter(processedKey.id);
+ }
+
+ return processedKey;
+ });
+ console.log('Processed API Keys:', this.apiKeys);
+ } else {
+ console.error('API Keys load failed:', data.message);
+ this.apiKeys = [];
+ }
+ } catch (error) {
+ console.error('Failed to load API keys:', error);
+ this.apiKeys = [];
+ } finally {
+ this.apiKeysLoading = false;
+ }
+ },
+
+ async loadAccounts() {
+ this.accountsLoading = true;
+ try {
+ const response = await fetch('/admin/claude-accounts', {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ this.accounts = data.data || [];
+ }
+ } catch (error) {
+ console.error('Failed to load accounts:', error);
+ } finally {
+ this.accountsLoading = false;
+ }
+ },
+
+
+ async loadModelStats() {
+ this.modelStatsLoading = true;
+ try {
+ const response = await fetch('/admin/model-stats?period=' + this.modelStatsPeriod, {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ this.modelStats = data.data || [];
+ } else {
+ this.modelStats = [];
+ }
+ } catch (error) {
+ console.error('Failed to load model stats:', error);
+ this.modelStats = [];
+ } finally {
+ this.modelStatsLoading = false;
+ }
+ },
+
+ async createApiKey() {
+ this.createApiKeyLoading = true;
+ try {
+ const response = await fetch('/admin/api-keys', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + this.authToken
+ },
+ body: JSON.stringify({
+ name: this.apiKeyForm.name,
+ tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.trim() ? parseInt(this.apiKeyForm.tokenLimit) : null,
+ description: this.apiKeyForm.description || ''
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ // 设置新API Key数据并显示弹窗
+ this.newApiKey = {
+ key: data.data.apiKey,
+ name: data.data.name,
+ description: data.data.description || '无描述',
+ showFullKey: false
+ };
+ this.showNewApiKeyModal = true;
+
+ // 关闭创建弹窗并清理表单
+ this.showCreateApiKeyModal = false;
+ this.apiKeyForm = { name: '', tokenLimit: '', description: '' };
+
+ // 重新加载API Keys列表
+ await this.loadApiKeys();
+ } else {
+ this.showToast(data.message || '创建失败', 'error', '创建失败');
+ }
+ } catch (error) {
+ console.error('Error creating API key:', error);
+ this.showToast('创建失败,请检查网络连接', 'error', '网络错误');
+ } finally {
+ this.createApiKeyLoading = false;
+ }
+ },
+
+ async deleteApiKey(keyId) {
+ if (!confirm('确定要删除这个 API Key 吗?')) return;
+
+ try {
+ const response = await fetch('/admin/api-keys/' + keyId, {
+ method: 'DELETE',
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showToast('API Key 删除成功', 'success', '删除成功');
+ await this.loadApiKeys();
+ } else {
+ this.showToast(data.message || '删除失败', 'error', '删除失败');
+ }
+ } catch (error) {
+ console.error('Error deleting API key:', error);
+ this.showToast('删除失败,请检查网络连接', 'error', '网络错误');
+ }
+ },
+
+ async deleteAccount(accountId) {
+ if (!confirm('确定要删除这个 Claude 账户吗?')) return;
+
+ try {
+ const response = await fetch('/admin/claude-accounts/' + accountId, {
+ method: 'DELETE',
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showToast('Claude 账户删除成功', 'success', '删除成功');
+ await this.loadAccounts();
+ } else {
+ this.showToast(data.message || '删除失败', 'error', '删除失败');
+ }
+ } catch (error) {
+ console.error('Error deleting account:', error);
+ this.showToast('删除失败,请检查网络连接', 'error', '网络错误');
+ }
+ },
+
+ // API Key 展示相关方法
+ toggleApiKeyVisibility() {
+ this.newApiKey.showFullKey = !this.newApiKey.showFullKey;
+ },
+
+ getDisplayedApiKey() {
+ if (this.newApiKey.showFullKey) {
+ return this.newApiKey.key;
+ } else {
+ // 显示前8个字符和后4个字符,中间用*代替
+ const key = this.newApiKey.key;
+ if (key.length <= 12) return key;
+ return key.substring(0, 8) + '●'.repeat(Math.max(0, key.length - 12)) + key.substring(key.length - 4);
+ }
+ },
+
+ async copyApiKeyToClipboard() {
+ try {
+ await navigator.clipboard.writeText(this.newApiKey.key);
+ this.showToast('API Key 已复制到剪贴板', 'success', '复制成功');
+ } catch (error) {
+ console.error('Failed to copy:', error);
+ // 降级方案:创建一个临时文本区域
+ const textArea = document.createElement('textarea');
+ textArea.value = this.newApiKey.key;
+ document.body.appendChild(textArea);
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ this.showToast('API Key 已复制到剪贴板', 'success', '复制成功');
+ } catch (fallbackError) {
+ this.showToast('复制失败,请手动复制', 'error', '复制失败');
+ }
+ document.body.removeChild(textArea);
+ }
+ },
+
+ closeNewApiKeyModal() {
+ // 显示确认提示
+ if (confirm('关闭后将无法再次查看完整的API Key,请确保已经妥善保存。确定要关闭吗?')) {
+ this.showNewApiKeyModal = false;
+ this.newApiKey = { key: '', name: '', description: '', showFullKey: false };
+ }
+ },
+
+
+ // 格式化数字,添加千分符
+ formatNumber(num) {
+ if (num === null || num === undefined) return '0';
+ return Number(num).toLocaleString();
+ },
+
+ // 格式化运行时间
+ formatUptime(seconds) {
+ if (!seconds) return '0s';
+
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+
+ if (days > 0) {
+ return days + '天' + hours + '时';
+ } else if (hours > 0) {
+ return hours + '时' + mins + '分';
+ } else {
+ return mins + '分';
+ }
+ },
+
+ // 计算百分比
+ calculatePercentage(value, stats) {
+ const total = stats.reduce((sum, stat) => sum + (stat.allTokens || 0), 0);
+ if (total === 0) return 0;
+ return ((value / total) * 100).toFixed(1);
+ },
+
+ // 加载仪表盘模型统计
+ async loadDashboardModelStats() {
+ console.log('Loading dashboard model stats, period:', this.dashboardModelPeriod, 'authToken:', !!this.authToken);
+ try {
+ const response = await fetch('/admin/model-stats?period=' + this.dashboardModelPeriod, {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+
+ console.log('Model stats response status:', response.status);
+
+ if (!response.ok) {
+ console.error('Model stats API error:', response.status, response.statusText);
+ const errorText = await response.text();
+ console.error('Error response:', errorText);
+ this.dashboardModelStats = [];
+ return;
+ }
+
+ const data = await response.json();
+ console.log('Model stats response data:', data);
+
+ if (data.success) {
+ this.dashboardModelStats = data.data || [];
+ console.log('Loaded model stats:', this.dashboardModelStats.length, 'items');
+ this.updateModelUsageChart();
+ } else {
+ console.warn('Model stats API returned success=false:', data);
+ this.dashboardModelStats = [];
+ }
+ } catch (error) {
+ console.error('Failed to load dashboard model stats:', error);
+ this.dashboardModelStats = [];
+ }
+ },
+
+ // 更新模型使用饼图
+ updateModelUsageChart() {
+
+ if (!this.dashboardModelStats.length) {
+ console.warn('No dashboard model stats data, skipping chart update');
+ return;
+ }
+
+ // 检查Chart.js是否已加载
+ if (typeof Chart === 'undefined') {
+ console.warn('Chart.js not loaded yet, retrying...');
+ setTimeout(() => this.updateModelUsageChart(), 500);
+ return;
+ }
+
+ // 严格检查DOM元素是否有效
+ if (!this.isElementValid('modelUsageChart')) {
+ console.error('Model usage chart canvas element not found or invalid');
+ return;
+ }
+
+ const ctx = document.getElementById('modelUsageChart');
+
+ // 安全销毁现有图表
+ if (this.modelUsageChart) {
+ try {
+ this.modelUsageChart.destroy();
+ } catch (error) {
+ console.warn('Error destroying model usage chart:', error);
+ }
+ this.modelUsageChart = null;
+ }
+
+ // 再次验证元素在销毁后仍然有效
+ if (!this.isElementValid('modelUsageChart')) {
+ console.error('Model usage chart canvas element became invalid after cleanup');
+ return;
+ }
+
+ const labels = this.dashboardModelStats.map(stat => stat.model);
+ const data = this.dashboardModelStats.map(stat => stat.allTokens || 0);
+
+
+ // 生成渐变色
+ const colors = [
+ 'rgba(102, 126, 234, 0.8)',
+ 'rgba(118, 75, 162, 0.8)',
+ 'rgba(240, 147, 251, 0.8)',
+ 'rgba(16, 185, 129, 0.8)',
+ 'rgba(245, 158, 11, 0.8)',
+ 'rgba(239, 68, 68, 0.8)'
+ ];
+
+ try {
+ // 最后一次检查元素有效性
+ if (!this.isElementValid('modelUsageChart')) {
+ throw new Error('Canvas element is not valid for chart creation');
+ }
+
+ this.modelUsageChart = new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: labels,
+ datasets: [{
+ data: data,
+ backgroundColor: colors,
+ borderColor: 'rgba(255, 255, 255, 1)',
+ borderWidth: 2
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false, // 禁用动画防止异步渲染问题
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ padding: 15,
+ font: {
+ size: 12
+ }
+ }
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ const label = context.label || '';
+ const value = context.parsed || 0;
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
+ const percentage = ((value / total) * 100).toFixed(1);
+ return label + ': ' + value.toLocaleString() + ' (' + percentage + '%)';
+ }
+ }
+ }
+ }
+ }
+ });
+ } catch (error) {
+ console.error('Error creating model usage chart:', error);
+ this.modelUsageChart = null;
+ }
+ },
+
+ // 设置趋势图周期(添加防抖)
+ setTrendPeriod: null, // 将在mounted中初始化为防抖函数
+
+ // 实际的设置趋势图周期方法
+ async _setTrendPeriod(days) {
+ console.log('Setting trend period to:', days);
+
+ // 先清理现有图表,防止竞态条件
+ if (this.usageTrendChart) {
+ try {
+ this.usageTrendChart.stop();
+ this.usageTrendChart.destroy();
+ } catch (error) {
+ console.warn('Error cleaning trend chart:', error);
+ }
+ this.usageTrendChart = null;
+ }
+
+ this.trendPeriod = days;
+ await this.loadUsageTrend();
+ },
+
+ // 加载使用趋势数据
+ async loadUsageTrend() {
+ console.log('Loading usage trend data, period:', this.trendPeriod, 'authToken:', !!this.authToken);
+ try {
+ const response = await fetch('/admin/usage-trend?days=' + this.trendPeriod, {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+
+ console.log('Usage trend response status:', response.status);
+
+ if (!response.ok) {
+ console.error('Usage trend API error:', response.status, response.statusText);
+ const errorText = await response.text();
+ console.error('Error response:', errorText);
+ return;
+ }
+
+ const data = await response.json();
+ console.log('Usage trend response data:', data);
+
+ if (data.success) {
+ this.trendData = data.data || [];
+ console.log('Loaded trend data:', this.trendData.length, 'items');
+ this.updateUsageTrendChart();
+ } else {
+ console.warn('Usage trend API returned success=false:', data);
+ }
+ } catch (error) {
+ console.error('Failed to load usage trend:', error);
+ }
+ },
+
+ // 更新使用趋势图
+ updateUsageTrendChart() {
+
+ // 检查Chart.js是否已加载
+ if (typeof Chart === 'undefined') {
+ console.warn('Chart.js not loaded yet, retrying...');
+ setTimeout(() => this.updateUsageTrendChart(), 500);
+ return;
+ }
+
+ // 严格检查DOM元素是否有效
+ if (!this.isElementValid('usageTrendChart')) {
+ console.error('Usage trend chart canvas element not found or invalid');
+ return;
+ }
+
+ const ctx = document.getElementById('usageTrendChart');
+
+ // 安全销毁现有图表
+ if (this.usageTrendChart) {
+ try {
+ this.usageTrendChart.destroy();
+ } catch (error) {
+ console.warn('Error destroying usage trend chart:', error);
+ }
+ this.usageTrendChart = null;
+ }
+
+ // 如果没有数据,不创建图表
+ if (!this.trendData || this.trendData.length === 0) {
+ console.warn('No trend data available, skipping chart creation');
+ return;
+ }
+
+ // 再次验证元素在销毁后仍然有效
+ if (!this.isElementValid('usageTrendChart')) {
+ console.error('Usage trend chart canvas element became invalid after cleanup');
+ return;
+ }
+
+ const labels = this.trendData.map(item => item.date);
+ const inputData = this.trendData.map(item => item.inputTokens || 0);
+ const outputData = this.trendData.map(item => item.outputTokens || 0);
+ const cacheCreateData = this.trendData.map(item => item.cacheCreateTokens || 0);
+ const cacheReadData = this.trendData.map(item => item.cacheReadTokens || 0);
+ const requestsData = this.trendData.map(item => item.requests || 0);
+ const costData = this.trendData.map(item => item.cost || 0);
+
+
+ try {
+ // 最后一次检查元素有效性
+ if (!this.isElementValid('usageTrendChart')) {
+ throw new Error('Canvas element is not valid for chart creation');
+ }
+
+ this.usageTrendChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: '输入Token',
+ data: inputData,
+ borderColor: 'rgb(102, 126, 234)',
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
+ tension: 0.3
+ },
+ {
+ label: '输出Token',
+ data: outputData,
+ borderColor: 'rgb(240, 147, 251)',
+ backgroundColor: 'rgba(240, 147, 251, 0.1)',
+ tension: 0.3
+ },
+ {
+ label: '缓存创建Token',
+ data: cacheCreateData,
+ borderColor: 'rgb(59, 130, 246)',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ tension: 0.3
+ },
+ {
+ label: '缓存读取Token',
+ data: cacheReadData,
+ borderColor: 'rgb(147, 51, 234)',
+ backgroundColor: 'rgba(147, 51, 234, 0.1)',
+ tension: 0.3
+ },
+ {
+ label: '费用 (USD)',
+ data: costData,
+ borderColor: 'rgb(34, 197, 94)',
+ backgroundColor: 'rgba(34, 197, 94, 0.1)',
+ tension: 0.3,
+ yAxisID: 'y2'
+ },
+ {
+ label: '请求数',
+ data: requestsData,
+ borderColor: 'rgb(16, 185, 129)',
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
+ tension: 0.3,
+ yAxisID: 'y1'
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false, // 禁用动画防止异步渲染问题
+ interaction: {
+ mode: 'index',
+ intersect: false,
+ },
+ scales: {
+ y: {
+ type: 'linear',
+ display: true,
+ position: 'left',
+ title: {
+ display: true,
+ text: 'Token数量'
+ }
+ },
+ y1: {
+ type: 'linear',
+ display: true,
+ position: 'right',
+ title: {
+ display: true,
+ text: '请求数'
+ },
+ grid: {
+ drawOnChartArea: false,
+ }
+ },
+ y2: {
+ type: 'linear',
+ display: false, // 隐藏费用轴,在tooltip中显示
+ position: 'right'
+ }
+ },
+ plugins: {
+ legend: {
+ position: 'top',
+ },
+ tooltip: {
+ mode: 'index',
+ intersect: false,
+ callbacks: {
+ label: function(context) {
+ const label = context.dataset.label || '';
+ let value = context.parsed.y;
+
+ if (label === '费用 (USD)') {
+ // 格式化费用显示
+ if (value < 0.01) {
+ return label + ': $' + value.toFixed(6);
+ } else {
+ return label + ': $' + value.toFixed(4);
+ }
+ } else if (label === '请求数') {
+ return label + ': ' + value.toLocaleString();
+ } else {
+ return label + ': ' + value.toLocaleString() + ' tokens';
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ } catch (error) {
+ console.error('Error creating usage trend chart:', error);
+ this.usageTrendChart = null;
+ }
+ },
+
+ // 切换API Key模型统计展开状态
+ toggleApiKeyModelStats(keyId) {
+ if (!keyId) {
+ console.warn('toggleApiKeyModelStats: keyId is null or undefined');
+ return;
+ }
+
+ console.log('Toggling API key model stats for:', keyId, 'current state:', this.expandedApiKeys[keyId]);
+
+ if (this.expandedApiKeys[keyId]) {
+ // 收起展开
+ this.expandedApiKeys = {
+ ...this.expandedApiKeys
+ };
+ delete this.expandedApiKeys[keyId];
+ } else {
+ // 展开并加载数据
+ this.expandedApiKeys = {
+ ...this.expandedApiKeys,
+ [keyId]: true
+ };
+ console.log('Expanded keys after toggle:', this.expandedApiKeys);
+ this.loadApiKeyModelStats(keyId);
+ }
+ },
+
+ // 加载API Key的模型统计
+ async loadApiKeyModelStats(keyId, forceReload = false) {
+ if (!keyId) {
+ console.warn('loadApiKeyModelStats: keyId is null or undefined');
+ return;
+ }
+
+ // 如果已经有数据且不为空,且不是强制重新加载,则跳过加载
+ if (!forceReload && this.apiKeyModelStats[keyId] && this.apiKeyModelStats[keyId].length > 0) {
+ console.log('API key model stats already loaded for:', keyId);
+ return;
+ }
+
+ const filter = this.getApiKeyDateFilter(keyId);
+ console.log('Loading API key model stats for:', keyId, 'period:', this.apiKeyModelPeriod, 'forceReload:', forceReload, 'authToken:', !!this.authToken);
+ console.log('API Key date filter:', filter);
+
+ // 清除现有数据以显示加载状态
+ if (forceReload) {
+ const newStats = { ...this.apiKeyModelStats };
+ delete newStats[keyId];
+ this.apiKeyModelStats = newStats;
+ }
+
+ try {
+ // 构建API请求URL,根据筛选类型传递不同参数
+ let url = '/admin/api-keys/' + keyId + '/model-stats';
+ const params = new URLSearchParams();
+
+ // 检查是否有具体的日期范围设置(包括快捷按钮设置的日期)
+ if (filter.customStart && filter.customEnd) {
+ // 有具体日期范围,使用自定义时间范围方式
+ params.append('startDate', filter.customStart);
+ params.append('endDate', filter.customEnd);
+ params.append('period', 'custom');
+ console.log('Using custom date range:', filter.customStart, 'to', filter.customEnd);
+ } else {
+ // 没有具体日期范围,使用预设期间(目前只有 today 会走这里)
+ const period = filter.preset === 'today' ? 'daily' : 'monthly';
+ params.append('period', period);
+ console.log('Using preset period:', period);
+ }
+
+ url += '?' + params.toString();
+ console.log('API request URL:', url);
+
+ const response = await fetch(url, {
+ headers: { 'Authorization': 'Bearer ' + this.authToken }
+ });
+
+ console.log('API key model stats response status:', response.status);
+
+ if (!response.ok) {
+ console.error('API key model stats API error:', response.status, response.statusText);
+ const errorText = await response.text();
+ console.error('Error response:', errorText);
+ return;
+ }
+
+ const data = await response.json();
+ console.log('API key model stats response data:', data);
+
+ if (data.success) {
+ console.log('API response success, data:', data.data);
+ console.log('Setting apiKeyModelStats for keyId:', keyId);
+
+ // 确保响应式更新 - 创建新对象
+ const newStats = { ...this.apiKeyModelStats };
+ newStats[keyId] = data.data || [];
+ this.apiKeyModelStats = newStats;
+
+ console.log('Updated apiKeyModelStats:', this.apiKeyModelStats);
+ console.log('Data for keyId', keyId, ':', this.apiKeyModelStats[keyId]);
+ console.log('Data length:', this.apiKeyModelStats[keyId] ? this.apiKeyModelStats[keyId].length : 'undefined');
+
+ // 确保Vue知道数据已经更新
+ this.$nextTick(() => {
+ console.log('Vue nextTick - stats should be visible now');
+ });
+ } else {
+ console.warn('API key model stats API returned success=false:', data);
+ }
+ } catch (error) {
+ console.error('Failed to load API key model stats:', error);
+ }
+ },
+
+ // 计算API Key模型使用百分比
+ calculateApiKeyModelPercentage(value, stats) {
+ const total = stats.reduce((sum, stat) => sum + (stat.allTokens || 0), 0);
+ if (total === 0) return 0;
+ return Math.round((value / total) * 100);
+ },
+
+ // 计算单个模型费用
+ calculateModelCost(stat) {
+ // 优先使用后端返回的费用数据
+ if (stat.formatted && stat.formatted.total) {
+ return stat.formatted.total;
+ }
+
+ // 如果后端没有返回费用数据,则使用简单估算(备用方案)
+ const inputTokens = stat.inputTokens || 0;
+ const outputTokens = stat.outputTokens || 0;
+ const cacheCreateTokens = stat.cacheCreateTokens || 0;
+ const cacheReadTokens = stat.cacheReadTokens || 0;
+
+ // 使用通用估算价格(Claude 3.5 Sonnet价格作为默认)
+ const inputCost = (inputTokens / 1000000) * 3.00;
+ const outputCost = (outputTokens / 1000000) * 15.00;
+ const cacheCreateCost = (cacheCreateTokens / 1000000) * 3.75;
+ const cacheReadCost = (cacheReadTokens / 1000000) * 0.30;
+
+ const totalCost = inputCost + outputCost + cacheCreateCost + cacheReadCost;
+
+ if (totalCost < 0.000001) return '$0.000000';
+ if (totalCost < 0.01) return '$' + totalCost.toFixed(6);
+ return '$' + totalCost.toFixed(4);
+ },
+
+ // 计算API Key费用
+ calculateApiKeyCost(usage) {
+ if (!usage || !usage.total) return '$0.000000';
+
+ // 使用通用模型价格估算
+ const totalInputTokens = usage.total.inputTokens || 0;
+ const totalOutputTokens = usage.total.outputTokens || 0;
+ const totalCacheCreateTokens = usage.total.cacheCreateTokens || 0;
+ const totalCacheReadTokens = usage.total.cacheReadTokens || 0;
+
+ // 简单估算(使用Claude 3.5 Sonnet价格)
+ const inputCost = (totalInputTokens / 1000000) * 3.00;
+ const outputCost = (totalOutputTokens / 1000000) * 15.00;
+ const cacheCreateCost = (totalCacheCreateTokens / 1000000) * 3.75;
+ const cacheReadCost = (totalCacheReadTokens / 1000000) * 0.30;
+
+ const totalCost = inputCost + outputCost + cacheCreateCost + cacheReadCost;
+
+ if (totalCost < 0.000001) return '$0.000000';
+ if (totalCost < 0.01) return '$' + totalCost.toFixed(6);
+ return '$' + totalCost.toFixed(4);
+ },
+
+ // 初始化日期筛选器
+ initializeDateFilter() {
+ console.log('Initializing date filter, default preset:', this.dateFilter.preset);
+
+ // 根据默认的日期筛选设置正确的 dashboardModelPeriod
+ if (this.dateFilter.preset === 'today') {
+ this.dashboardModelPeriod = 'daily';
+ } else {
+ this.dashboardModelPeriod = 'monthly';
+ }
+
+ console.log('Set dashboardModelPeriod to:', this.dashboardModelPeriod);
+ },
+
+ // 日期筛选方法
+ setDateFilterPreset(preset) {
+ this.dateFilter.type = 'preset';
+ this.dateFilter.preset = preset;
+ // 清除自定义日期范围
+ this.dateFilter.customStart = '';
+ this.dateFilter.customEnd = '';
+
+ // 根据预设计算并设置自定义时间框的值
+ const option = this.dateFilter.presetOptions.find(opt => opt.value === preset);
+ if (option) {
+ const today = new Date();
+ const startDate = new Date(today);
+ startDate.setDate(today.getDate() - (option.days - 1));
+
+ // 格式化为 Element Plus 需要的格式
+ const formatDate = (date) => {
+ return date.getFullYear() + '-' +
+ String(date.getMonth() + 1).padStart(2, '0') + '-' +
+ String(date.getDate()).padStart(2, '0') + ' 00:00:00';
+ };
+
+ this.dateFilter.customRange = [
+ formatDate(startDate),
+ formatDate(today)
+ ];
+ }
+
+ this.refreshChartsData();
+ },
+
+ // 获取今日日期字符串
+ getTodayDate() {
+ return new Date().toISOString().split('T')[0];
+ },
+
+ // 获取自定义范围天数
+ getCustomRangeDays() {
+ if (!this.dateFilter.customStart || !this.dateFilter.customEnd) return 0;
+ const start = new Date(this.dateFilter.customStart);
+ const end = new Date(this.dateFilter.customEnd);
+ return Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+ },
+
+ // 验证并设置自定义日期范围
+ validateAndSetCustomRange() {
+ if (!this.dateFilter.customStart || !this.dateFilter.customEnd) return;
+
+ const start = new Date(this.dateFilter.customStart);
+ const end = new Date(this.dateFilter.customEnd);
+ const today = new Date();
+
+ // 确保结束日期不晚于今天
+ if (end > today) {
+ this.dateFilter.customEnd = this.getTodayDate();
+ end.setTime(today.getTime());
+ }
+
+ // 确保开始日期不晚于结束日期
+ if (start > end) {
+ this.dateFilter.customStart = this.dateFilter.customEnd;
+ start.setTime(end.getTime());
+ }
+
+ // 限制最大31天
+ const daysDiff = this.getCustomRangeDays();
+ if (daysDiff > 31) {
+ // 自动调整开始日期,保持31天范围
+ const newStart = new Date(end);
+ newStart.setDate(end.getDate() - 30); // 31天范围
+ this.dateFilter.customStart = newStart.toISOString().split('T')[0];
+
+ this.showToast('日期范围已自动调整为最大31天', 'warning', '范围限制');
+ }
+
+ // 只有在都有效时才更新
+ if (this.dateFilter.customStart && this.dateFilter.customEnd) {
+ this.dateFilter.type = 'custom';
+ this.refreshChartsData();
+ }
+ },
+
+ setDateFilterCustom() {
+ this.validateAndSetCustomRange();
+ },
+
+ // 一体化日期范围选择器相关方法
+ toggleDateRangePicker() {
+ this.showDateRangePicker = !this.showDateRangePicker;
+ },
+
+ getDateRangeDisplayText() {
+ if (this.dateFilter.type === 'preset') {
+ const option = this.dateFilter.presetOptions.find(opt => opt.value === this.dateFilter.preset);
+ return option ? option.label : '自定义范围';
+ } else if (this.dateFilter.customStart && this.dateFilter.customEnd) {
+ const start = new Date(this.dateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ const end = new Date(this.dateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ return start + ' - ' + end + ' (' + this.getCustomRangeDays() + '天)';
+ }
+ return '选择日期范围';
+ },
+
+ getCustomDateRangeText() {
+ if (this.dateFilter.type === 'custom' && this.dateFilter.customStart && this.dateFilter.customEnd) {
+ const start = new Date(this.dateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ const end = new Date(this.dateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ return start + ' - ' + end;
+ }
+ return '自定义范围';
+ },
+
+ onDateRangeChange() {
+ // 实时验证日期范围
+ if (this.dateFilter.customStart && this.dateFilter.customEnd) {
+ const start = new Date(this.dateFilter.customStart);
+ const end = new Date(this.dateFilter.customEnd);
+ const today = new Date();
+
+ // 确保结束日期不晚于今天
+ if (end > today) {
+ this.dateFilter.customEnd = this.getTodayDate();
+ }
+
+ // 确保开始日期不晚于结束日期
+ if (start > end) {
+ this.dateFilter.customStart = this.dateFilter.customEnd;
+ }
+
+ // 限制最大31天
+ const daysDiff = this.getCustomRangeDays();
+ if (daysDiff > 31) {
+ const newStart = new Date(end);
+ newStart.setDate(end.getDate() - 30);
+ this.dateFilter.customStart = newStart.toISOString().split('T')[0];
+ }
+ }
+ },
+
+ clearDateRange() {
+ this.dateFilter.customStart = '';
+ this.dateFilter.customEnd = '';
+ this.dateFilter.type = 'preset';
+ this.dateFilter.preset = '7days'; // 恢复默认
+ },
+
+ applyDateRange() {
+ if (this.dateFilter.customStart && this.dateFilter.customEnd) {
+ this.dateFilter.type = 'custom';
+ this.dateFilter.preset = ''; // 清除预设选择
+ this.showDateRangePicker = false;
+ this.refreshChartsData();
+ } else {
+ this.showToast('请选择完整的日期范围', 'warning', '日期范围');
+ }
+ },
+
+ refreshChartsData() {
+ // 根据当前日期筛选设置更新数据
+ let days;
+ if (this.dateFilter.type === 'preset') {
+ const option = this.dateFilter.presetOptions.find(opt => opt.value === this.dateFilter.preset);
+ days = option ? option.days : 7;
+
+ // 设置模型统计期间
+ if (this.dateFilter.preset === 'today') {
+ this.dashboardModelPeriod = 'daily';
+ } else {
+ this.dashboardModelPeriod = 'monthly';
+ }
+ } else {
+ // 自定义日期范围
+ const start = new Date(this.dateFilter.customStart);
+ const end = new Date(this.dateFilter.customEnd);
+ days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+ this.dashboardModelPeriod = 'daily'; // 自定义范围使用日统计
+ }
+
+ this.trendPeriod = days;
+
+ // 重新加载数据
+ this.loadDashboardModelStats();
+ this.loadUsageTrend();
+ },
+
+ // API Keys 日期筛选方法
+ setApiKeyDateFilterPreset(preset, keyId) {
+ console.log('Setting API Key date filter preset:', preset, 'for keyId:', keyId);
+
+ const filter = this.getApiKeyDateFilter(keyId);
+ console.log('Before preset change - type:', filter.type, 'preset:', filter.preset);
+
+ filter.type = 'preset';
+ filter.preset = preset;
+
+ // 根据预设计算并设置具体的日期范围
+ const option = filter.presetOptions.find(opt => opt.value === preset);
+ if (option) {
+ const today = new Date();
+ const startDate = new Date(today);
+ startDate.setDate(today.getDate() - (option.days - 1));
+
+ // 设置为日期字符串格式 YYYY-MM-DD
+ filter.customStart = startDate.toISOString().split('T')[0];
+ filter.customEnd = today.toISOString().split('T')[0];
+
+ // 同时设置customRange,让日期选择器显示当前选中的范围
+ // 格式化为 Element Plus 需要的格式
+ const formatDate = (date) => {
+ return date.getFullYear() + '-' +
+ String(date.getMonth() + 1).padStart(2, '0') + '-' +
+ String(date.getDate()).padStart(2, '0') + ' 00:00:00';
+ };
+
+ filter.customRange = [
+ formatDate(startDate),
+ formatDate(today)
+ ];
+
+ console.log('Set customStart to:', filter.customStart);
+ console.log('Set customEnd to:', filter.customEnd);
+ console.log('Set customRange to:', filter.customRange);
+ }
+
+ console.log('After preset change - type:', filter.type, 'preset:', filter.preset);
+
+ // 立即加载数据
+ this.loadApiKeyModelStats(keyId, true);
+ },
+
+ validateAndSetApiKeyCustomRange(keyId) {
+ const filter = this.getApiKeyDateFilter(keyId);
+
+ if (!filter.customStart || !filter.customEnd) return;
+
+ const start = new Date(filter.customStart);
+ const end = new Date(filter.customEnd);
+ const today = new Date();
+
+ // 确保结束日期不晚于今天
+ if (end > today) {
+ filter.customEnd = this.getTodayDate();
+ end.setTime(today.getTime());
+ }
+
+ // 确保开始日期不晚于结束日期
+ if (start > end) {
+ filter.customStart = filter.customEnd;
+ start.setTime(end.getTime());
+ }
+
+ // 限制最大31天
+ const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+ if (daysDiff > 31) {
+ // 自动调整开始日期,保持31天范围
+ const newStart = new Date(end);
+ newStart.setDate(end.getDate() - 30); // 31天范围
+ filter.customStart = newStart.toISOString().split('T')[0];
+
+ this.showToast('日期范围已自动调整为最大31天', 'warning', '范围限制');
+ }
+
+ // 只有在都有效时才更新
+ if (filter.customStart && filter.customEnd) {
+ filter.type = 'custom';
+ this.apiKeyModelPeriod = 'daily'; // 自定义范围使用日统计
+
+ // 强制重新加载该API Key的数据
+ this.loadApiKeyModelStats(keyId, true);
+ }
+ },
+
+ // API Keys 一体化日期范围选择器相关方法
+ toggleApiKeyDateRangePicker() {
+ this.showApiKeyDateRangePicker = !this.showApiKeyDateRangePicker;
+ },
+
+ getApiKeyDateRangeDisplayText() {
+ if (this.apiKeyDateFilter.type === 'preset') {
+ const option = this.apiKeyDateFilter.presetOptions.find(opt => opt.value === this.apiKeyDateFilter.preset);
+ return option ? option.label : '自定义';
+ } else if (this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) {
+ const start = new Date(this.apiKeyDateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ const end = new Date(this.apiKeyDateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ return start + ' - ' + end;
+ }
+ return '自定义';
+ },
+
+ getApiKeyCustomDateRangeText() {
+ if (this.apiKeyDateFilter.type === 'custom' && this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) {
+ const start = new Date(this.apiKeyDateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ const end = new Date(this.apiKeyDateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'});
+ return start + ' - ' + end;
+ }
+ return '自定义范围';
+ },
+
+ getApiKeyCustomRangeDays() {
+ if (!this.apiKeyDateFilter.customStart || !this.apiKeyDateFilter.customEnd) return 0;
+ const start = new Date(this.apiKeyDateFilter.customStart);
+ const end = new Date(this.apiKeyDateFilter.customEnd);
+ return Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+ },
+
+ onApiKeyDateRangeChange() {
+ if (this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) {
+ const start = new Date(this.apiKeyDateFilter.customStart);
+ const end = new Date(this.apiKeyDateFilter.customEnd);
+ const today = new Date();
+
+ // 确保结束日期不晚于今天
+ if (end > today) {
+ this.apiKeyDateFilter.customEnd = this.getTodayDate();
+ }
+
+ // 确保开始日期不晚于结束日期
+ if (start > end) {
+ this.apiKeyDateFilter.customStart = this.apiKeyDateFilter.customEnd;
+ }
+
+ // 限制最大31天
+ const daysDiff = this.getApiKeyCustomRangeDays();
+ if (daysDiff > 31) {
+ const newStart = new Date(end);
+ newStart.setDate(end.getDate() - 30);
+ this.apiKeyDateFilter.customStart = newStart.toISOString().split('T')[0];
+ }
+ }
+ },
+
+ clearApiKeyDateRange() {
+ this.apiKeyDateFilter.customStart = '';
+ this.apiKeyDateFilter.customEnd = '';
+ this.apiKeyDateFilter.type = 'preset';
+ this.apiKeyDateFilter.preset = '7days'; // 恢复默认
+ },
+
+ applyApiKeyDateRange(keyId) {
+ if (this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) {
+ this.apiKeyDateFilter.type = 'custom';
+ this.apiKeyDateFilter.preset = ''; // 清除预设选择
+ this.apiKeyModelPeriod = 'daily'; // 自定义范围使用日统计
+ this.showApiKeyDateRangePicker = false;
+
+ // 强制重新加载该API Key的数据
+ this.loadApiKeyModelStats(keyId, true);
+ } else {
+ this.showToast('请选择完整的日期范围', 'warning', '日期范围');
+ }
+ },
+
+ // Element Plus 日期选择器相关方法
+
+ // 禁用未来日期
+ disabledDate(date) {
+ return date > new Date();
+ },
+
+ // 仪表盘自定义日期范围变化处理
+ onCustomDateRangeChange(value) {
+ if (value && value.length === 2) {
+ // 清除快捷选择的焦点状态
+ this.dateFilter.type = 'custom';
+ this.dateFilter.preset = '';
+ this.dateFilter.customStart = value[0].split(' ')[0];
+ this.dateFilter.customEnd = value[1].split(' ')[0];
+
+ // 检查日期范围限制
+ const start = new Date(value[0]);
+ const end = new Date(value[1]);
+ const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+
+ if (daysDiff > 31) {
+ this.showToast('日期范围不能超过31天', 'warning', '范围限制');
+ // 重置为默认7天
+ this.dateFilter.customRange = null;
+ this.dateFilter.type = 'preset';
+ this.dateFilter.preset = '7days';
+ return;
+ }
+
+ this.refreshChartsData();
+ } else if (value === null) {
+ // 清空时恢复默认
+ this.dateFilter.type = 'preset';
+ this.dateFilter.preset = '7days';
+ this.dateFilter.customStart = '';
+ this.dateFilter.customEnd = '';
+ this.refreshChartsData();
+ }
+ },
+
+ // API Keys自定义日期范围变化处理
+ onApiKeyCustomDateRangeChange(keyId) {
+ return (value) => {
+ const filter = this.getApiKeyDateFilter(keyId);
+ console.log('API Key custom date range change:', value, 'for keyId:', keyId);
+ console.log('Before change - type:', filter.type, 'preset:', filter.preset);
+
+ // 更新 customRange 值
+ filter.customRange = value;
+
+ if (value && value.length === 2) {
+ // 清除快捷选择的焦点状态
+ filter.type = 'custom';
+ filter.preset = ''; // 清空preset确保快捷按钮失去焦点
+ filter.customStart = value[0].split(' ')[0];
+ filter.customEnd = value[1].split(' ')[0];
+
+ console.log('After change - type:', filter.type, 'preset:', filter.preset);
+ console.log('Set customStart to:', filter.customStart);
+ console.log('Set customEnd to:', filter.customEnd);
+
+ // 检查日期范围限制
+ const start = new Date(value[0]);
+ const end = new Date(value[1]);
+ const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
+
+ if (daysDiff > 31) {
+ this.showToast('日期范围不能超过31天', 'warning', '范围限制');
+ // 重置到默认7天
+ this.resetApiKeyDateFilterToDefault(keyId);
+ return;
+ }
+
+ // 立即加载数据
+ console.log('Loading model stats after date range selection');
+ this.loadApiKeyModelStats(keyId, true);
+ } else if (value === null || value === undefined) {
+ // 清空时恢复默认7天
+ this.resetApiKeyDateFilterToDefault(keyId);
+
+ console.log('Cleared - type:', filter.type, 'preset:', filter.preset);
+
+ // 加载数据
+ this.loadApiKeyModelStats(keyId, true);
+ }
+ };
+ },
+
+ // 初始化API Key的日期筛选器
+ initApiKeyDateFilter(keyId) {
+ const today = new Date();
+ const startDate = new Date(today);
+ startDate.setDate(today.getDate() - 6); // 7天前
+
+ // Vue 3 直接赋值即可,不需要 $set
+ this.apiKeyDateFilters[keyId] = {
+ type: 'preset',
+ preset: '7days',
+ customStart: startDate.toISOString().split('T')[0],
+ customEnd: today.toISOString().split('T')[0],
+ customRange: null,
+ presetOptions: this.apiKeyDateFilterDefaults.presetOptions
+ };
+ },
+
+ // 获取API Key的日期筛选器状态
+ getApiKeyDateFilter(keyId) {
+ if (!this.apiKeyDateFilters[keyId]) {
+ this.initApiKeyDateFilter(keyId);
+ }
+ return this.apiKeyDateFilters[keyId];
+ },
+
+ // 重置API Key日期筛选器为默认值(内部使用)
+ resetApiKeyDateFilterToDefault(keyId) {
+ const filter = this.getApiKeyDateFilter(keyId);
+
+ // 重置为默认的7天预设
+ filter.type = 'preset';
+ filter.preset = '7days';
+ filter.customRange = null;
+
+ // 计算7天的具体日期范围
+ const today = new Date();
+ const startDate = new Date(today);
+ startDate.setDate(today.getDate() - 6); // 7天前
+
+ filter.customStart = startDate.toISOString().split('T')[0];
+ filter.customEnd = today.toISOString().split('T')[0];
+
+ console.log(`Reset API Key ${keyId} to default 7 days range:`, filter.customStart, 'to', filter.customEnd);
+ },
+
+ // 重置API Key日期筛选器并刷新
+ resetApiKeyDateFilter(keyId) {
+ console.log('Resetting API Key date filter for keyId:', keyId);
+
+ this.resetApiKeyDateFilterToDefault(keyId);
+
+ // 使用nextTick确保状态更新后再加载数据
+ this.$nextTick(() => {
+ this.loadApiKeyModelStats(keyId, true);
+ });
+
+ this.showToast('已重置筛选条件并刷新数据', 'info', '重置成功');
+ }
+ }
+});
+
+// 使用Element Plus,确保正确的语言包配置
+if (typeof ElementPlus !== 'undefined') {
+ app.use(ElementPlus, {
+ locale: typeof ElementPlusLocaleZhCn !== 'undefined' ? ElementPlusLocaleZhCn : undefined
+ });
+} else {
+ console.warn('Element Plus 未正确加载');
+}
+
+// 挂载应用
+app.mount('#app');
\ No newline at end of file
diff --git a/web/admin/index.html b/web/admin/index.html
new file mode 100644
index 00000000..efdcc92d
--- /dev/null
+++ b/web/admin/index.html
@@ -0,0 +1,2058 @@
+
+
+
+
+
+ Claude Relay Service - 管理后台
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loginError }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tab.name }}
+
+
+
+
+
+
+
+
+
+
+
总API Keys
+
{{ dashboardData.totalApiKeys }}
+
活跃: {{ dashboardData.activeApiKeys || 0 }}
+
+
+
+
+
+
+
+
+
+
+
Claude账户
+
{{ dashboardData.totalAccounts }}
+
活跃: {{ dashboardData.activeAccounts || 0 }}
+
+
+
+
+
+
+
+
+
+
+
今日请求
+
{{ dashboardData.todayRequests }}
+
总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}
+
+
+
+
+
+
+
+
+
+
+
系统状态
+
{{ dashboardData.systemStatus }}
+
运行时间: {{ formatUptime(dashboardData.uptime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
今日Token
+
+
{{ formatNumber((dashboardData.todayInputTokens || 0) + (dashboardData.todayOutputTokens || 0) + (dashboardData.todayCacheCreateTokens || 0) + (dashboardData.todayCacheReadTokens || 0)) }}
+
/ {{ costsData.todayCosts.formatted.totalCost }}
+
+
+
+ 输入: {{ formatNumber(dashboardData.todayInputTokens || 0) }}
+ 输出: {{ formatNumber(dashboardData.todayOutputTokens || 0) }}
+ 缓存创建: {{ formatNumber(dashboardData.todayCacheCreateTokens || 0) }}
+ 缓存读取: {{ formatNumber(dashboardData.todayCacheReadTokens || 0) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
总Token消耗
+
+
{{ formatNumber((dashboardData.totalInputTokens || 0) + (dashboardData.totalOutputTokens || 0) + (dashboardData.totalCacheCreateTokens || 0) + (dashboardData.totalCacheReadTokens || 0)) }}
+
/ {{ costsData.totalCosts.formatted.totalCost }}
+
+
+
+ 输入: {{ formatNumber(dashboardData.totalInputTokens || 0) }}
+ 输出: {{ formatNumber(dashboardData.totalOutputTokens || 0) }}
+ 缓存创建: {{ formatNumber(dashboardData.totalCacheCreateTokens || 0) }}
+ 缓存读取: {{ formatNumber(dashboardData.totalCacheReadTokens || 0) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
平均RPM
+
{{ dashboardData.systemRPM || 0 }}
+
每分钟请求数
+
+
+
+
+
+
+
+
+
+
+
平均TPM
+
{{ dashboardData.systemTPM || 0 }}
+
每分钟Token数
+
+
+
+
+
+
+
+
+
+
+
+
模型使用分布与Token使用趋势
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
详细统计数据
+
+
+
+
+
+ 模型
+ 请求数
+ 总Token
+ 费用
+ 占比
+
+
+
+
+ {{ stat.model }}
+ {{ formatNumber(stat.requests) }}
+ {{ formatNumber(stat.allTokens) }}
+ {{ stat.formatted ? stat.formatted.total : '$0.000000' }}
+
+
+ {{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
API Keys 管理
+
管理和监控您的 API 密钥
+
+
+ 创建新 Key
+
+
+
+
+
+
+
+
+
+
暂无 API Keys
+
点击上方按钮创建您的第一个 API Key
+
+
+
+
+
+
+ 名称
+ API Key
+ 状态
+ 使用统计
+ 创建时间
+ 操作
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ key.name }}
+
{{ key.id }}
+
+
+
+
+
+ {{ (key.apiKey || '').substring(0, 20) }}...
+
+
+
+
+
+ {{ key.isActive ? '活跃' : '禁用' }}
+
+
+
+
+
+
+ 请求数:
+ {{ formatNumber((key.usage && key.usage.total && key.usage.total.requests) || 0) }}
+
+
+
+ Token:
+ {{ formatNumber((key.usage && key.usage.total && key.usage.total.tokens) || 0) }}
+
+
+
+ 费用:
+ {{ calculateApiKeyCost(key.usage) }}
+
+
+
+ 输入: {{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}
+ 输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}
+
+
+
+ RPM: {{ (key.usage && key.usage.averages && key.usage.averages.rpm) || 0 }}
+ TPM: {{ (key.usage && key.usage.averages && key.usage.averages.tpm) || 0 }}
+
+
+
+
+ 今日: {{ formatNumber((key.usage && key.usage.daily && key.usage.daily.requests) || 0) }}次
+ {{ formatNumber((key.usage && key.usage.daily && key.usage.daily.tokens) || 0) }}T
+
+
+
+
+
+
+ 模型使用分布
+
+
+
+
+
+ {{ new Date(key.createdAt).toLocaleDateString() }}
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 模型使用分布
+
+
+
+ {{ apiKeyModelStats[key.id].length }} 个模型
+
+
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
onApiKeyCustomDateRangeChange(key.id)(value)"
+ type="datetimerange"
+ range-separator="至"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ :disabled-date="disabledDate"
+ size="small"
+ style="width: 280px;"
+ class="api-key-date-picker"
+ :clearable="true"
+ :unlink-panels="false"
+ @visible-change="(visible) => !visible && $forceUpdate()"
+ >
+
+
+
+
+
+
+
+
+
暂无模型使用数据
+
+
+ 刷新
+
+
+
尝试调整时间范围或点击刷新重新加载数据
+
+
+
+
+
+ {{ stat.model }}
+ {{ stat.requests }} 次请求
+
+
+
+
+
+
+
+ 总Token:
+
+ {{ formatNumber(stat.allTokens) }}
+
+
+
+
+ 费用:
+
+ {{ calculateModelCost(stat) }}
+
+
+
+
+ 输入:
+
+ {{ formatNumber(stat.inputTokens) }}
+
+
+
+
+ 输出:
+
+ {{ formatNumber(stat.outputTokens) }}
+
+
+
+
+
+ 缓存创建:
+
+ {{ formatNumber(stat.cacheCreateTokens) }}
+
+
+
+
+ 缓存读取:
+
+ {{ formatNumber(stat.cacheReadTokens) }}
+
+
+
+
+
+
+
+
+ {{ calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) }}%
+
+
+
+
+
+
+
+
+
+
+ 总计统计
+
+
+
+ 总请求: {{ apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.requests, 0) }}
+
+
+ 总Token: {{ formatNumber(apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.allTokens, 0)) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Claude 账户管理
+
管理您的 Claude 账户和代理配置
+
+
+ 添加账户
+
+
+
+
+
+
+
+
+
+
暂无 Claude 账户
+
点击上方按钮添加您的第一个账户
+
+
+
+
+
+
+ 名称
+ 类型
+ 状态
+ 代理
+ 最后使用
+ 操作
+
+
+
+
+
+
+
+
+
+
+
{{ account.name }}
+
{{ account.id }}
+
+
+
+
+
+ OAuth
+
+
+ 传统
+
+
+
+
+
+ {{ account.isActive ? '正常' : '异常' }}
+
+
+
+
+ {{ account.proxy.type }}://{{ account.proxy.host }}:{{ account.proxy.port }}
+
+ 无代理
+
+
+ {{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
模型使用统计
+
查看不同模型的使用量和费用统计
+
+
+
+ 今日
+ 本月
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ stat.model }}
+
{{ stat.period === 'daily' ? '今日使用' : '本月使用' }}
+
+
+
{{ (stat.formatted && stat.formatted.total) || '$0.000000' }}
+
总费用
+
+
+
+
+
+
{{ formatNumber((stat.usage && stat.usage.requests) || 0) }}
+
请求数
+
+
+
{{ formatNumber((stat.usage && stat.usage.inputTokens) || 0) }}
+
输入Token
+
+
+
{{ formatNumber((stat.usage && stat.usage.outputTokens) || 0) }}
+
输出Token
+
+
+
{{ formatNumber((stat.usage && stat.usage.totalTokens) || 0) }}
+
总Token
+
+
+
+
+
+
+
{{ formatNumber((stat.usage && stat.usage.cacheCreateTokens) || 0) }}
+
缓存创建
+
+
+
{{ formatNumber((stat.usage && stat.usage.cacheReadTokens) || 0) }}
+
缓存读取
+
+
+
+
+
+
+
{{ (stat.formatted && stat.formatted.input) || '$0.000000' }}
+
输入费用
+
+
+
{{ (stat.formatted && stat.formatted.output) || '$0.000000' }}
+
输出费用
+
+
+
{{ (stat.formatted && stat.formatted.cacheWrite) || '$0.000000' }}
+
缓存写入
+
+
+
{{ (stat.formatted && stat.formatted.cacheRead) || '$0.000000' }}
+
缓存读取
+
+
+
+
+
+
定价信息 (USD per 1M tokens):
+
+
输入: ${{ (stat.pricing && stat.pricing.input) || 0 }}
+
输出: ${{ (stat.pricing && stat.pricing.output) || 0 }}
+
缓存写: ${{ (stat.pricing && stat.pricing.cacheWrite) || 0 }}
+
缓存读: ${{ (stat.pricing && stat.pricing.cacheRead) || 0 }}
+
+
+
+
+
+
+
+
+
+
+
+ Claude Code 使用教程
+
+
跟着这个教程,你可以轻松在自己的电脑上安装并使用 Claude Code。
+
+
+
+
+
+
+
+ {{ system.name }}
+
+
+
+
+
+
+
+
+
+ 1
+ 安装 Node.js 环境
+
+
Claude Code 需要 Node.js 环境才能运行。
+
+
+
+
+ Windows 安装方法
+
+
+
方法一:官网下载(推荐)
+
+ 打开浏览器访问 https://nodejs.org/
+ 点击 "LTS" 版本进行下载(推荐长期支持版本)
+ 下载完成后双击 .msi 文件
+ 按照安装向导完成安装,保持默认设置即可
+
+
+
+
方法二:使用包管理器
+
如果你安装了 Chocolatey 或 Scoop,可以使用命令行安装:
+
+
# 使用 Chocolatey
+
choco install nodejs
+
# 或使用 Scoop
+
scoop install nodejs
+
+
+
+
Windows 注意事项
+
+ • 建议使用 PowerShell 而不是 CMD
+ • 如果遇到权限问题,尝试以管理员身份运行
+ • 某些杀毒软件可能会误报,需要添加白名单
+
+
+
+
+
+
+
验证安装是否成功
+
安装完成后,打开 PowerShell 或 CMD,输入以下命令:
+
+
node --version
+
npm --version
+
+
如果显示版本号,说明安装成功了!
+
+
+
+
+
+
+ 2
+ 安装 Git Bash
+
+
Windows 环境下需要使用 Git Bash 安装Claude code。安装完成后,环境变量设置和使用 Claude Code 仍然在普通的 PowerShell 或 CMD 中进行。
+
+
+
+
+ 下载并安装 Git for Windows
+
+
+ 访问 https://git-scm.com/downloads/win
+ 点击 "Download for Windows" 下载安装包
+ 运行下载的 .exe 安装文件
+ 在安装过程中保持默认设置,直接点击 "Next" 完成安装
+
+
+
安装完成后
+
+ • 在任意文件夹右键可以看到 "Git Bash Here" 选项
+ • 也可以从开始菜单启动 "Git Bash"
+ • 只需要在 Git Bash 中运行 npm install 命令
+ • 后续的环境变量设置和使用都在 PowerShell/CMD 中
+
+
+
+
+
+
+
验证 Git Bash 安装
+
打开 Git Bash,输入以下命令验证:
+
+
如果显示 Git 版本号,说明安装成功!
+
+
+
+
+
+
+ 3
+ 安装 Claude Code
+
+
+
+
+
+ 安装 Claude Code
+
+
打开 Git Bash(重要:不要使用 PowerShell),运行以下命令:
+
+
# 在 Git Bash 中全局安装 Claude Code
+
npm install -g @anthropic-ai/claude-code
+
+
这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。
+
+
+
重要提醒
+
+ • 必须在 Git Bash 中运行,不要在 PowerShell 中运行
+ • 如果遇到权限问题,可以尝试在 Git Bash 中使用 sudo 命令
+
+
+
+
+
+
+
验证 Claude Code 安装
+
安装完成后,输入以下命令检查是否安装成功:
+
+
如果显示版本号,恭喜你!Claude Code 已经成功安装了。
+
+
+
+
+
+
+ 4
+ 设置环境变量
+
+
+
+
+
+ 配置 Claude Code 环境变量
+
+
为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:
+
+
+
+
方法一:PowerShell 临时设置(推荐)
+
在 PowerShell 中运行以下命令:
+
+
$env:ANTHROPIC_BASE_URL = "{{ currentBaseUrl }}"
+
$env:ANTHROPIC_AUTH_TOKEN = "你的API密钥"
+
+
💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。
+
+
+
+
方法二:系统环境变量(永久设置)
+
+ 右键"此电脑" → "属性" → "高级系统设置"
+ 点击"环境变量"按钮
+ 在"用户变量"或"系统变量"中点击"新建"
+ 添加以下两个变量:
+
+
+
+ 变量名: ANTHROPIC_BASE_URL
+ 变量值: {{ currentBaseUrl }}
+
+
+ 变量名: ANTHROPIC_AUTH_TOKEN
+ 变量值: 你的API密钥
+
+
+
+
+
+
+
+
+
验证环境变量设置
+
设置完环境变量后,可以通过以下命令验证是否设置成功:
+
+
+
+
在 PowerShell 中验证:
+
+
echo $env:ANTHROPIC_BASE_URL
+
echo $env:ANTHROPIC_AUTH_TOKEN
+
+
+
+
+
在 CMD 中验证:
+
+
echo %ANTHROPIC_BASE_URL%
+
echo %ANTHROPIC_AUTH_TOKEN%
+
+
+
+
+
+
+ 预期输出示例:
+
+
+
{{ currentBaseUrl }}
+
cr_xxxxxxxxxxxxxxxxxx
+
+
+ 💡 如果输出为空或显示变量名本身,说明环境变量设置失败,请重新设置。
+
+
+
+
+
+
+
+
+ 5
+ 开始使用 Claude Code
+
+
+
现在你可以开始使用 Claude Code 了!
+
+
+
+
+
+
在特定项目中使用
+
+
# 进入你的项目目录
+
cd C:\path\to\your\project
+
# 启动 Claude Code
+
claude
+
+
+
+
+
+
+
+
+
+
+ Windows 常见问题解决
+
+
+
+
+ 安装时提示 "permission denied" 错误
+
+
+
这通常是权限问题,尝试以下解决方法:
+
+ 以管理员身份运行 PowerShell
+ 或者配置 npm 使用用户目录:npm config set prefix %APPDATA%\npm
+
+
+
+
+
+
+ PowerShell 执行策略错误
+
+
+
如果遇到执行策略限制,运行:
+
+
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+
+
+
+
+
+ 环境变量设置后不生效
+
+
+
设置永久环境变量后需要:
+
+ 重新启动 PowerShell 或 CMD
+ 或者注销并重新登录 Windows
+ 验证设置:echo $env:ANTHROPIC_BASE_URL
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 安装 Node.js 环境
+
+
Claude Code 需要 Node.js 环境才能运行。
+
+
+
+
+ macOS 安装方法
+
+
+
方法一:使用 Homebrew(推荐)
+
如果你已经安装了 Homebrew,使用它安装 Node.js 会更方便:
+
+
# 更新 Homebrew
+
brew update
+
# 安装 Node.js
+
brew install node
+
+
+
+
方法二:官网下载
+
+ 访问 https://nodejs.org/
+ 下载适合 macOS 的 LTS 版本
+ 打开下载的 .pkg 文件
+ 按照安装程序指引完成安装
+
+
+
+
macOS 注意事项
+
+ • 如果遇到权限问题,可能需要使用 sudo
+ • 首次运行可能需要在系统偏好设置中允许
+ • 建议使用 Terminal 或 iTerm2
+
+
+
+
+
+
+
验证安装是否成功
+
安装完成后,打开 Terminal,输入以下命令:
+
+
node --version
+
npm --version
+
+
如果显示版本号,说明安装成功了!
+
+
+
+
+
+
+ 2
+ 安装 Claude Code
+
+
+
+
+
+ 安装 Claude Code
+
+
打开 Terminal,运行以下命令:
+
+
# 全局安装 Claude Code
+
npm install -g @anthropic-ai/claude-code
+
+
如果遇到权限问题,可以使用 sudo:
+
+
sudo npm install -g @anthropic-ai/claude-code
+
+
+
+
+
+
验证 Claude Code 安装
+
安装完成后,输入以下命令检查是否安装成功:
+
+
如果显示版本号,恭喜你!Claude Code 已经成功安装了。
+
+
+
+
+
+
+ 3
+ 设置环境变量
+
+
+
+
+
+ 配置 Claude Code 环境变量
+
+
为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:
+
+
+
+
方法一:临时设置(当前会话)
+
在 Terminal 中运行以下命令:
+
+
export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"
+
export ANTHROPIC_AUTH_TOKEN="你的API密钥"
+
+
💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。
+
+
+
+
方法二:永久设置
+
编辑你的 shell 配置文件(根据你使用的 shell):
+
+
# 对于 zsh (默认)
+
echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.zshrc
+
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc
+
source ~/.zshrc
+
+
+
# 对于 bash
+
echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.bash_profile
+
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bash_profile
+
source ~/.bash_profile
+
+
+
+
+
+
+
+
+
+ 4
+ 开始使用 Claude Code
+
+
+
现在你可以开始使用 Claude Code 了!
+
+
+
+
+
+
在特定项目中使用
+
+
# 进入你的项目目录
+
cd /path/to/your/project
+
# 启动 Claude Code
+
claude
+
+
+
+
+
+
+
+
+
+
+ macOS 常见问题解决
+
+
+
+
+ 安装时提示权限错误
+
+
+
尝试以下解决方法:
+
+ 使用 sudo 安装:sudo npm install -g @anthropic-ai/claude-code
+ 或者配置 npm 使用用户目录:npm config set prefix ~/.npm-global
+
+
+
+
+
+
+ macOS 安全设置阻止运行
+
+
+
如果系统阻止运行 Claude Code:
+
+ 打开"系统偏好设置" → "安全性与隐私"
+ 点击"仍要打开"或"允许"
+ 或者在 Terminal 中运行:sudo spctl --master-disable
+
+
+
+
+
+
+ 环境变量不生效
+
+
+
检查以下几点:
+
+ 确认修改了正确的配置文件(.zshrc 或 .bash_profile)
+ 重新启动 Terminal
+ 验证设置:echo $ANTHROPIC_BASE_URL
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 安装 Node.js 环境
+
+
Claude Code 需要 Node.js 环境才能运行。
+
+
+
+
+ Linux 安装方法
+
+
+
方法一:使用官方仓库(推荐)
+
+
# 添加 NodeSource 仓库
+
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
+
# 安装 Node.js
+
sudo apt-get install -y nodejs
+
+
+
+
方法二:使用系统包管理器
+
虽然版本可能不是最新的,但对于基本使用已经足够:
+
+
# Ubuntu/Debian
+
sudo apt update
+
sudo apt install nodejs npm
+
# CentOS/RHEL/Fedora
+
sudo dnf install nodejs npm
+
+
+
+
Linux 注意事项
+
+ • 某些发行版可能需要安装额外的依赖
+ • 如果遇到权限问题,使用 sudo
+ • 确保你的用户在 npm 的全局目录有写权限
+
+
+
+
+
+
+
验证安装是否成功
+
安装完成后,打开终端,输入以下命令:
+
+
node --version
+
npm --version
+
+
如果显示版本号,说明安装成功了!
+
+
+
+
+
+
+ 2
+ 安装 Claude Code
+
+
+
+
+
+ 安装 Claude Code
+
+
打开终端,运行以下命令:
+
+
# 全局安装 Claude Code
+
npm install -g @anthropic-ai/claude-code
+
+
如果遇到权限问题,可以使用 sudo:
+
+
sudo npm install -g @anthropic-ai/claude-code
+
+
+
+
+
+
验证 Claude Code 安装
+
安装完成后,输入以下命令检查是否安装成功:
+
+
如果显示版本号,恭喜你!Claude Code 已经成功安装了。
+
+
+
+
+
+
+ 3
+ 设置环境变量
+
+
+
+
+
+ 配置 Claude Code 环境变量
+
+
为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:
+
+
+
+
方法一:临时设置(当前会话)
+
在终端中运行以下命令:
+
+
export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"
+
export ANTHROPIC_AUTH_TOKEN="你的API密钥"
+
+
💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。
+
+
+
+
方法二:永久设置
+
编辑你的 shell 配置文件:
+
+
# 对于 bash (默认)
+
echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.bashrc
+
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bashrc
+
source ~/.bashrc
+
+
+
# 对于 zsh
+
echo 'export ANTHROPIC_BASE_URL="{{ currentBaseUrl }}"' >> ~/.zshrc
+
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc
+
source ~/.zshrc
+
+
+
+
+
+
+
+
+
+ 4
+ 开始使用 Claude Code
+
+
+
现在你可以开始使用 Claude Code 了!
+
+
+
+
+
+
在特定项目中使用
+
+
# 进入你的项目目录
+
cd /path/to/your/project
+
# 启动 Claude Code
+
claude
+
+
+
+
+
+
+
+
+
+
+ Linux 常见问题解决
+
+
+
+
+ 安装时提示权限错误
+
+
+
尝试以下解决方法:
+
+ 使用 sudo 安装:sudo npm install -g @anthropic-ai/claude-code
+ 或者配置 npm 使用用户目录:npm config set prefix ~/.npm-global
+ 然后添加到 PATH:export PATH=~/.npm-global/bin:$PATH
+
+
+
+
+
+
+ 缺少依赖库
+
+
+
某些 Linux 发行版需要安装额外依赖:
+
+
# Ubuntu/Debian
+
sudo apt install build-essential
+
# CentOS/RHEL
+
sudo dnf groupinstall "Development Tools"
+
+
+
+
+
+
+ 环境变量不生效
+
+
+
检查以下几点:
+
+ 确认修改了正确的配置文件(.bashrc 或 .zshrc)
+ 重新启动终端或运行 source ~/.bashrc
+ 验证设置:echo $ANTHROPIC_BASE_URL
+
+
+
+
+
+
+
+
+
+
🎉 恭喜你!
+
你已经成功安装并配置了 Claude Code,现在可以开始享受 AI 编程助手带来的便利了。
+
如果在使用过程中遇到任何问题,可以查看官方文档或社区讨论获取帮助。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
API Key 创建成功
+
请妥善保存您的 API Key
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
重要提醒
+
+ 这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
+
+
+
+
+
+
+
+
+
API Key 名称
+
+ {{ newApiKey.name }}
+
+
+
+
+
备注
+
+ {{ newApiKey.description }}
+
+
+
+
+
API Key
+
+
+ {{ getDisplayedApiKey() }}
+
+
+
+
+
+
+
+
+
+ 点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
+
+
+
+
+
+
+
+
+ 复制 API Key
+
+
+ 我已保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 账户名称
+
+
+
+
+ 描述 (可选)
+
+
+
+
+
代理设置 (可选)
+
如果需要使用代理访问Claude服务,请配置代理信息。OAuth授权也将通过此代理进行。
+
+ 不使用代理
+ SOCKS5 代理
+ HTTP 代理
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 下一步
+
+
+
+
+
+
+
+
+
+
+
+
+
获取授权链接
+
点击下方按钮生成OAuth授权链接
+
+
+
+ {{ authUrlLoading ? '生成中...' : '生成授权链接' }}
+
+
+
+
+
+
+
+
+
+
+
+
操作说明
+
+ 点击下方的授权链接,在新页面中完成Claude Code登录
+ 点击"授权"按钮同意应用权限
+ 页面会显示一个 Authorization Code
+ 复制这个 Authorization Code 并粘贴到下方输入框
+ 💡 提示: 请直接粘贴显示的Authorization Code
+
+
+
+
+
+
+
+
+
+
+ Authorization Code
+
+
+
+
+ 请粘贴从Claude页面复制的Authorization Code
+
+
+
+
+
+
+
+
+ 上一步
+
+
+
+
+ {{ createAccountLoading ? '创建中...' : '完成创建' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ toast.title }}
+
{{ toast.message }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/admin/style.css b/web/admin/style.css
new file mode 100644
index 00000000..f05122e1
--- /dev/null
+++ b/web/admin/style.css
@@ -0,0 +1,395 @@
+:root {
+ --primary-color: #667eea;
+ --secondary-color: #764ba2;
+ --accent-color: #f093fb;
+ --success-color: #10b981;
+ --warning-color: #f59e0b;
+ --error-color: #ef4444;
+ --surface-color: rgba(255, 255, 255, 0.95);
+ --glass-color: rgba(255, 255, 255, 0.1);
+ --text-primary: #1f2937;
+ --text-secondary: #6b7280;
+ --border-color: rgba(255, 255, 255, 0.2);
+}
+
+/* 通用transition - 仅应用于特定元素 */
+body, div, button, input, select, textarea, table, tr, td, th, span, p, h1, h2, h3, h4, h5, h6 {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 50%, var(--accent-color) 100%);
+ background-attachment: fixed;
+ min-height: 100vh;
+ margin: 0;
+ overflow-x: hidden;
+}
+
+body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 20% 80%, rgba(240, 147, 251, 0.2) 0%, transparent 50%),
+ radial-gradient(circle at 80% 20%, rgba(102, 126, 234, 0.2) 0%, transparent 50%),
+ radial-gradient(circle at 40% 40%, rgba(118, 75, 162, 0.1) 0%, transparent 50%);
+ pointer-events: none;
+ z-index: -1;
+}
+
+.glass {
+ background: var(--glass-color);
+ backdrop-filter: blur(20px);
+ border: 1px solid var(--border-color);
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.1),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.04),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+.glass-strong {
+ background: var(--surface-color);
+ backdrop-filter: blur(25px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow:
+ 0 25px 50px -12px rgba(0, 0, 0, 0.25),
+ 0 0 0 1px rgba(255, 255, 255, 0.05),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+.tab-btn {
+ position: relative;
+ overflow: hidden;
+ border-radius: 12px;
+ font-weight: 500;
+ letter-spacing: 0.025em;
+}
+
+.tab-btn::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left 0.5s;
+}
+
+.tab-btn:hover::before {
+ left: 100%;
+}
+
+.tab-btn.active {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+ color: white;
+ box-shadow:
+ 0 10px 15px -3px rgba(102, 126, 234, 0.3),
+ 0 4px 6px -2px rgba(102, 126, 234, 0.05);
+ transform: translateY(-1px);
+}
+
+.card {
+ background: var(--surface-color);
+ border-radius: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ box-shadow:
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ overflow: hidden;
+ position: relative;
+}
+
+.card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
+}
+
+.stat-card {
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
+ border-radius: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ padding: 24px;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.stat-card::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-4px);
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.1),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.stat-card:hover::before {
+ opacity: 1;
+}
+
+.stat-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+ color: white;
+ box-shadow:
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.btn {
+ font-weight: 500;
+ border-radius: 12px;
+ border: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+ letter-spacing: 0.025em;
+}
+
+.btn::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 0;
+ height: 0;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ transition: width 0.3s ease, height 0.3s ease;
+}
+
+.btn:active::before {
+ width: 300px;
+ height: 300px;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+ color: white;
+ box-shadow:
+ 0 10px 15px -3px rgba(102, 126, 234, 0.3),
+ 0 4px 6px -2px rgba(102, 126, 234, 0.05);
+}
+
+.btn-primary:hover {
+ transform: translateY(-1px);
+ box-shadow:
+ 0 20px 25px -5px rgba(102, 126, 234, 0.3),
+ 0 10px 10px -5px rgba(102, 126, 234, 0.1);
+}
+
+.btn-success {
+ background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%);
+ color: white;
+ box-shadow:
+ 0 10px 15px -3px rgba(16, 185, 129, 0.3),
+ 0 4px 6px -2px rgba(16, 185, 129, 0.05);
+}
+
+.btn-success:hover {
+ transform: translateY(-1px);
+ box-shadow:
+ 0 20px 25px -5px rgba(16, 185, 129, 0.3),
+ 0 10px 10px -5px rgba(16, 185, 129, 0.1);
+}
+
+.btn-danger {
+ background: linear-gradient(135deg, var(--error-color) 0%, #dc2626 100%);
+ color: white;
+ box-shadow:
+ 0 10px 15px -3px rgba(239, 68, 68, 0.3),
+ 0 4px 6px -2px rgba(239, 68, 68, 0.05);
+}
+
+.btn-danger:hover {
+ transform: translateY(-1px);
+ box-shadow:
+ 0 20px 25px -5px rgba(239, 68, 68, 0.3),
+ 0 10px 10px -5px rgba(239, 68, 68, 0.1);
+}
+
+.form-input {
+ background: rgba(255, 255, 255, 0.9);
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-radius: 12px;
+ padding: 16px;
+ font-size: 16px;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(10px);
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow:
+ 0 0 0 3px rgba(102, 126, 234, 0.1),
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ background: rgba(255, 255, 255, 0.95);
+}
+
+.table-container {
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow:
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.table-row {
+ transition: all 0.2s ease;
+}
+
+.table-row:hover {
+ background: rgba(102, 126, 234, 0.05);
+ transform: scale(1.005);
+}
+
+.modal {
+ backdrop-filter: blur(8px);
+ background: rgba(0, 0, 0, 0.4);
+}
+
+.modal-content {
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 24px;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow:
+ 0 25px 50px -12px rgba(0, 0, 0, 0.25),
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(20px);
+}
+
+.header-title {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+}
+
+.loading-spinner {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top: 2px solid white;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.fade-enter-active, .fade-leave-active {
+ transition: opacity 0.3s ease;
+}
+
+.fade-enter-from, .fade-leave-to {
+ opacity: 0;
+}
+
+.slide-up-enter-active, .slide-up-leave-active {
+ transition: all 0.3s ease;
+}
+
+.slide-up-enter-from {
+ opacity: 0;
+ transform: translateY(30px);
+}
+
+.slide-up-leave-to {
+ opacity: 0;
+ transform: translateY(-30px);
+}
+
+.toast {
+ position: fixed;
+ top: 80px;
+ right: 20px;
+ z-index: 1000;
+ min-width: 320px;
+ max-width: 500px;
+ transform: translateX(100%);
+ transition: transform 0.3s ease-in-out;
+}
+
+.toast.show {
+ transform: translateX(0);
+}
+
+.toast-success {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ color: white;
+ border: 1px solid rgba(16, 185, 129, 0.3);
+}
+
+.toast-error {
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ color: white;
+ border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+.toast-info {
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
+ color: white;
+ border: 1px solid rgba(59, 130, 246, 0.3);
+}
+
+.toast-warning {
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
+ color: white;
+ border: 1px solid rgba(245, 158, 11, 0.3);
+}
+
+[v-cloak] {
+ display: none;
+}
+
+
+
+@media (max-width: 768px) {
+ .glass, .glass-strong {
+ margin: 16px;
+ border-radius: 20px;
+ }
+
+ .stat-card {
+ padding: 16px;
+ }
+
+ .tab-btn {
+ font-size: 14px;
+ padding: 12px 8px;
+ }
+}
\ No newline at end of file