Browse Source

first commit

Taio_O 3 months ago
commit
6335594577
49 changed files with 16489 additions and 0 deletions
  1. 71 0
      .gitignore
  2. 312 0
      README.md
  3. 99 0
      config/example_clash_config.yaml
  4. 31 0
      env.example
  5. 90 0
      get-chat-id.js
  6. 166 0
      go-analysis.md
  7. 240 0
      go-speed-test/README.md
  8. 284 0
      go-speed-test/USAGE.md
  9. 25 0
      go-speed-test/config.yaml
  10. 56 0
      go-speed-test/go.mod
  11. 571 0
      go-speed-test/go.sum
  12. 287 0
      go-speed-test/internal/api/server.go
  13. 139 0
      go-speed-test/internal/config/config.go
  14. 143 0
      go-speed-test/internal/core/scheduler.go
  15. 492 0
      go-speed-test/internal/core/speed_tester.go
  16. 136 0
      go-speed-test/internal/database/database.go
  17. 85 0
      go-speed-test/internal/logger/logger.go
  18. 82 0
      go-speed-test/main.go
  19. 24 0
      go-speed-test/run.bat
  20. 21 0
      go-speed-test/run.sh
  21. 225 0
      go-speed-test/templates/index.html
  22. 87 0
      go-speed-test/test_speed.go
  23. 7749 0
      package-lock.json
  24. 49 0
      package.json
  25. 865 0
      public/app.js
  26. 464 0
      public/index.html
  27. 389 0
      public/styles.css
  28. 15 0
      public/sw.js
  29. 418 0
      src/api/routes.js
  30. 176 0
      src/app.js
  31. 23 0
      src/config/database.js
  32. 225 0
      src/core/clashParser.js
  33. 310 0
      src/core/notifier.js
  34. 309 0
      src/core/scheduler.js
  35. 517 0
      src/core/speedTester.js
  36. 235 0
      src/core/subscriptionManager.js
  37. 62 0
      src/database/migrate.js
  38. 136 0
      src/models/Node.js
  39. 82 0
      src/models/Notification.js
  40. 87 0
      src/models/TestResult.js
  41. 32 0
      src/models/index.js
  42. 61 0
      src/utils/logger.js
  43. 112 0
      start.js
  44. 157 0
      test-advanced-proxy.js
  45. 23 0
      test-direct-connection.js
  46. 79 0
      test-simple-proxy.js
  47. 77 0
      test-subscription-content.js
  48. 61 0
      test-subscription.js
  49. 110 0
      test-telegram.js

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# 依赖
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# 环境变量
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# 日志
+logs/
+*.log
+
+# 运行时数据
+pids/
+*.pid
+*.seed
+*.pid.lock
+
+# 覆盖率目录
+coverage/
+.nyc_output
+
+# 依赖目录
+jspm_packages/
+
+# 可选npm缓存目录
+.npm
+
+# 可选eslint缓存
+.eslintcache
+
+# 输出目录
+dist/
+build/
+
+# 临时文件
+tmp/
+temp/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# 数据库
+*.sqlite
+*.db
+
+# 配置文件
+config/clash_config.yaml
+config/*.yaml
+!config/example_clash_config.yaml
+
+# PM2
+.pm2/ 

+ 312 - 0
README.md

@@ -0,0 +1,312 @@
+# Clash节点测速工具
+
+一个基于Node.js的Clash节点测速工具,支持定时监控、Telegram通知和自动订阅更新。
+
+## 功能特性
+
+- 🔄 **定时测速**: 每15分钟自动测试所有节点
+- 📊 **多维度测试**: 延迟、速度、连接性测试
+- 🔔 **智能通知**: Telegram机器人故障和恢复通知
+- 📈 **数据统计**: 详细的测试历史和性能统计
+- 🌐 **API接口**: 完整的RESTful API
+- 📁 **配置导入**: 支持Clash配置文件导入
+- 🔄 **自动订阅**: 支持订阅地址自动更新节点
+- 🗄️ **数据持久化**: MySQL数据库存储
+
+## 技术栈
+
+- **后端**: Node.js + Express
+- **数据库**: MySQL + Sequelize ORM
+- **定时任务**: node-cron
+- **通知**: Telegram Bot API
+- **日志**: Winston
+- **配置解析**: YAML
+
+## 快速开始
+
+### 1. 环境要求
+
+- Node.js 16+
+- MySQL 8.0+
+- 可访问的Telegram Bot Token
+
+### 2. 安装依赖
+
+```bash
+npm install
+```
+
+### 3. 配置环境变量
+
+复制 `env.example` 为 `.env` 并修改配置:
+
+```bash
+cp env.example .env
+```
+
+```env
+# 数据库配置
+DB_HOST=localhost
+DB_PORT=3306
+DB_NAME=clash_speed_test
+DB_USER=root
+DB_PASSWORD=your_password
+
+# Telegram机器人配置
+TELEGRAM_BOT_TOKEN=7804040067:AAFeV7WeCWHUJTJzLUnN83C0Wid0pGSj6NQ
+TELEGRAM_CHAT_ID=your_chat_id
+
+# 应用配置
+PORT=3000
+NODE_ENV=development
+
+# Clash配置
+CLASH_CONFIG_PATH=./config/clash_config.yaml
+CLASH_API_URL=http://127.0.0.1:9090
+CLASH_SUBSCRIPTION_URL=http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c
+
+# 测速配置
+SPEED_TEST_INTERVAL=15
+SPEED_TEST_TIMEOUT=10000
+SPEED_TEST_URLS=https://www.google.com,https://www.youtube.com,https://www.github.com
+
+# 通知配置
+NOTIFICATION_FAILURE_THRESHOLD=3
+NOTIFICATION_RECOVERY_THRESHOLD=2
+```
+
+### 4. 创建数据库
+
+```sql
+CREATE DATABASE clash_speed_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+```
+
+### 5. 运行数据库迁移
+
+```bash
+npm run migrate
+```
+
+### 6. 启动应用
+
+```bash
+# 开发模式
+npm run dev
+
+# 生产模式
+npm start
+```
+
+## API接口
+
+### 节点管理
+
+- `GET /api/nodes` - 获取节点列表
+- `GET /api/nodes/:id` - 获取节点详情
+- `POST /api/nodes` - 创建节点
+- `PUT /api/nodes/:id` - 更新节点
+- `DELETE /api/nodes/:id` - 删除节点
+
+### 配置导入
+
+- `POST /api/import/clash` - 导入Clash配置
+
+### 订阅管理
+
+- `POST /api/subscription/update` - 手动更新订阅
+- `GET /api/subscription/status` - 获取订阅状态
+
+### 测试管理
+
+- `POST /api/test/manual` - 手动触发测试
+- `GET /api/test/results` - 获取测试结果
+
+### 通知管理
+
+- `GET /api/notifications` - 获取通知列表
+- `POST /api/notifications/test` - 测试通知连接
+
+### 统计信息
+
+- `GET /api/stats` - 获取统计信息
+- `GET /api/status` - 获取系统状态
+
+## 使用说明
+
+### 1. 创建Telegram机器人
+
+1. 在Telegram中搜索 `@BotFather`
+2. 发送 `/newbot` 创建新机器人
+3. 获取Bot Token
+4. 将机器人添加到目标群组或频道
+5. 获取Chat ID
+
+**快速获取Chat ID:**
+```bash
+npm run get-chat-id
+```
+
+**测试机器人功能:**
+```bash
+npm run test-telegram
+```
+
+### 2. 导入Clash配置
+
+```bash
+# 通过API导入
+curl -X POST http://localhost:3000/api/import/clash \
+  -H "Content-Type: application/json" \
+  -d '{
+    "configPath": "/path/to/your/clash/config.yaml"
+  }'
+
+# 或通过URL导入
+curl -X POST http://localhost:3000/api/import/clash \
+  -H "Content-Type: application/json" \
+  -d '{
+    "configUrl": "https://example.com/clash-config.yaml"
+  }'
+```
+
+### 3. 订阅管理
+
+```bash
+# 手动更新订阅
+curl -X POST http://localhost:3000/api/subscription/update
+
+# 获取订阅状态
+curl -X GET http://localhost:3000/api/subscription/status
+```
+
+### 4. 手动测试
+
+```bash
+# 测试所有节点
+curl -X POST http://localhost:3000/api/test/manual
+
+# 测试指定节点
+curl -X POST http://localhost:3000/api/test/manual \
+  -H "Content-Type: application/json" \
+  -d '{
+    "nodeIds": [1, 2, 3]
+  }'
+```
+
+## 项目结构
+
+```
+speed-test/
+├── src/
+│   ├── api/              # API路由
+│   ├── config/           # 配置文件
+│   ├── core/             # 核心功能
+│   ├── database/         # 数据库相关
+│   ├── models/           # 数据模型
+│   └── utils/            # 工具函数
+├── logs/                 # 日志文件
+├── config/               # 配置文件目录
+├── package.json
+├── env.example
+└── README.md
+```
+
+## 配置说明
+
+### 测速配置
+
+- `SPEED_TEST_INTERVAL`: 测速间隔(分钟)
+- `SPEED_TEST_TIMEOUT`: 测试超时时间(毫秒)
+- `SPEED_TEST_URLS`: 测试目标网站(逗号分隔)
+
+### 通知配置
+
+- `NOTIFICATION_FAILURE_THRESHOLD`: 故障通知阈值
+- `NOTIFICATION_RECOVERY_THRESHOLD`: 恢复通知阈值
+
+### 订阅配置
+
+- `CLASH_SUBSCRIPTION_URL`: Clash订阅地址
+- `SUBSCRIPTION_UPDATE_INTERVAL`: 订阅更新间隔(毫秒)
+
+## 部署
+
+### Docker部署
+
+```dockerfile
+FROM node:16-alpine
+
+WORKDIR /app
+
+COPY package*.json ./
+RUN npm ci --only=production
+
+COPY . .
+
+EXPOSE 3000
+
+CMD ["npm", "start"]
+```
+
+### PM2部署
+
+```bash
+npm install -g pm2
+pm2 start src/app.js --name clash-speed-test
+pm2 save
+pm2 startup
+```
+
+## 监控和维护
+
+### 日志查看
+
+```bash
+# 查看应用日志
+tail -f logs/combined.log
+
+# 查看错误日志
+tail -f logs/error.log
+```
+
+### 数据清理
+
+系统会自动清理:
+- 30天前的测试结果
+- 7天前的已发送通知
+
+### 性能优化
+
+- 数据库索引已自动创建
+- 测试结果分页查询
+- 并发测试限制
+
+## 故障排除
+
+### 常见问题
+
+1. **数据库连接失败**
+   - 检查数据库配置
+   - 确保数据库服务运行
+
+2. **Telegram通知失败**
+   - 检查Bot Token和Chat ID
+   - 确保机器人有发送消息权限
+
+3. **节点测试失败**
+   - 检查网络连接
+   - 验证节点配置正确性
+
+### 调试模式
+
+```bash
+NODE_ENV=development npm run dev
+```
+
+## 贡献
+
+欢迎提交Issue和Pull Request!
+
+## 许可证
+
+MIT License 

+ 99 - 0
config/example_clash_config.yaml

@@ -0,0 +1,99 @@
+# Clash配置文件示例
+# 用于测试导入功能
+
+port: 7890
+socks-port: 7891
+allow-lan: true
+mode: Rule
+log-level: info
+external-controller: 127.0.0.1:9090
+
+proxies:
+  # Shadowsocks节点示例
+  - name: "SS-Node-1"
+    type: ss
+    server: 1.2.3.4
+    port: 8388
+    cipher: aes-256-gcm
+    password: your_password
+    group: "SS-Nodes"
+
+  - name: "SS-Node-2"
+    type: ss
+    server: 5.6.7.8
+    port: 8388
+    cipher: chacha20-ietf-poly1305
+    password: your_password
+    group: "SS-Nodes"
+
+  # Vmess节点示例
+  - name: "VMess-Node-1"
+    type: vmess
+    server: 9.10.11.12
+    port: 443
+    uuid: your-uuid-here
+    alterId: 0
+    cipher: auto
+    tls: true
+    network: ws
+    ws-opts:
+      path: /path
+      headers:
+        Host: example.com
+    group: "VMess-Nodes"
+
+  - name: "VMess-Node-2"
+    type: vmess
+    server: 13.14.15.16
+    port: 443
+    uuid: your-uuid-here
+    alterId: 0
+    cipher: auto
+    tls: true
+    sni: example.com
+    group: "VMess-Nodes"
+
+  # Trojan节点示例
+  - name: "Trojan-Node-1"
+    type: trojan
+    server: 17.18.19.20
+    port: 443
+    password: your_password
+    sni: example.com
+    group: "Trojan-Nodes"
+
+  # HTTP代理示例
+  - name: "HTTP-Proxy-1"
+    type: http
+    server: 21.22.23.24
+    port: 8080
+    username: your_username
+    password: your_password
+    group: "HTTP-Proxies"
+
+proxy-groups:
+  - name: "Auto"
+    type: url-test
+    proxies:
+      - SS-Node-1
+      - SS-Node-2
+      - VMess-Node-1
+      - VMess-Node-2
+      - Trojan-Node-1
+    url: http://www.gstatic.com/generate_204
+    interval: 300
+
+  - name: "Fallback"
+    type: fallback
+    proxies:
+      - SS-Node-1
+      - SS-Node-2
+      - VMess-Node-1
+    url: http://www.gstatic.com/generate_204
+    interval: 300
+
+rules:
+  - DOMAIN-SUFFIX,google.com,Auto
+  - DOMAIN-SUFFIX,youtube.com,Auto
+  - DOMAIN-SUFFIX,github.com,Auto
+  - MATCH,DIRECT 

+ 31 - 0
env.example

@@ -0,0 +1,31 @@
+# 数据库配置
+DB_HOST=38.60.200.74
+DB_PORT=3306
+DB_NAME=speed_test
+DB_USER=speed_test
+DB_PASSWORD=BjKhwBRAGSfKc4e6
+
+# Telegram机器人配置
+TELEGRAM_BOT_TOKEN=7804040067:AAFeV7WeCWHUJTJzLUnN83C0Wid0pGSj6NQ
+TELEGRAM_CHAT_ID=-4840979728
+
+# 应用配置
+PORT=3000
+NODE_ENV=development
+
+# Clash配置
+CLASH_CONFIG_PATH=./config/clash_config.yaml
+CLASH_API_URL=http://127.0.0.1:9090
+CLASH_SUBSCRIPTION_URL=http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c
+
+# 测速配置
+SPEED_TEST_INTERVAL=5 # 分钟
+SPEED_TEST_TIMEOUT=10000 # 毫秒
+SPEED_TEST_URLS=https://www.google.com,https://www.youtube.com,https://www.github.com
+
+# 通知配置
+NOTIFICATION_FAILURE_THRESHOLD=3 # 连续失败次数阈值
+NOTIFICATION_RECOVERY_THRESHOLD=2 # 恢复通知阈值
+
+# 订阅配置
+SUBSCRIPTION_UPDATE_INTERVAL=3600000 # 订阅更新间隔(毫秒,默认1小时) 

+ 90 - 0
get-chat-id.js

@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+
+require('dotenv').config();
+const TelegramBot = require('node-telegram-bot-api');
+
+async function getChatId() {
+  console.log('🔍 获取Telegram Chat ID...\n');
+
+  const botToken = process.env.TELEGRAM_BOT_TOKEN;
+
+  if (!botToken) {
+    console.error('❌ 未配置TELEGRAM_BOT_TOKEN');
+    process.exit(1);
+  }
+
+  try {
+    console.log('📋 机器人配置信息:');
+    console.log(`   Bot Token: ${botToken.substring(0, 10)}...${botToken.substring(botToken.length - 10)}\n`);
+
+    // 创建机器人实例
+    const bot = new TelegramBot(botToken, { polling: false });
+
+    console.log('🔄 测试机器人连接...');
+    const me = await bot.getMe();
+    console.log('✅ 机器人连接成功!');
+    console.log(`   机器人名称: ${me.first_name}`);
+    console.log(`   机器人用户名: @${me.username}\n`);
+
+    console.log('📋 获取最近的更新...');
+    const updates = await bot.getUpdates();
+    
+    if (updates.length === 0) {
+      console.log('⚠️  没有找到任何更新记录');
+      console.log('\n📝 请按以下步骤操作:');
+      console.log('   1. 将机器人添加到群组或频道');
+      console.log('   2. 在群组中发送 /start 或任何消息');
+      console.log('   3. 再次运行此脚本');
+      return;
+    }
+
+    console.log(`✅ 找到 ${updates.length} 条更新记录\n`);
+
+    console.log('📊 Chat ID 列表:');
+    const chatIds = new Set();
+    
+    updates.forEach((update, index) => {
+      if (update.message) {
+        const chat = update.message.chat;
+        chatIds.add(chat.id);
+        console.log(`   ${index + 1}. Chat ID: ${chat.id}`);
+        console.log(`      类型: ${chat.type}`);
+        console.log(`      标题: ${chat.title || chat.first_name || '个人聊天'}`);
+        console.log(`      用户名: ${chat.username ? '@' + chat.username : '无'}`);
+        console.log('');
+      }
+    });
+
+    if (chatIds.size > 0) {
+      console.log('💡 建议使用的Chat ID:');
+      chatIds.forEach((chatId, index) => {
+        console.log(`   ${index + 1}. ${chatId}`);
+      });
+      
+      console.log('\n📝 配置方法:');
+      console.log('   在.env文件中设置:');
+      console.log(`   TELEGRAM_CHAT_ID=${Array.from(chatIds)[0]}`);
+    }
+
+  } catch (error) {
+    console.error('❌ 获取Chat ID失败:', error.message);
+    
+    if (error.response) {
+      console.error('   错误详情:', error.response.data);
+    }
+    
+    console.log('\n🔧 故障排除建议:');
+    console.log('   1. 检查Bot Token是否正确');
+    console.log('   2. 确保机器人已添加到群组/频道');
+    console.log('   3. 在群组中发送消息激活机器人');
+    
+    process.exit(1);
+  }
+}
+
+// 如果直接运行此文件
+if (require.main === module) {
+  getChatId();
+}
+
+module.exports = { getChatId }; 

+ 166 - 0
go-analysis.md

@@ -0,0 +1,166 @@
+# 为什么 Go 语言更适合 Clash 测速项目
+
+## 当前 Node.js 方案的问题
+
+### 1. 协议支持问题
+- **Shadowsocks**: Node.js 缺少成熟的 SS 客户端库
+- **VMess**: 需要实现复杂的 VMess 协议
+- **Trojan**: 需要 TLS 代理支持
+- **ShadowsocksR**: 协议更复杂,Node.js 支持更少
+
+### 2. 性能问题
+- **并发处理**: JavaScript 单线程模型,大量并发连接性能差
+- **内存使用**: Node.js 内存占用较高
+- **网络 I/O**: 异步处理不如 Go 的 goroutine 高效
+
+### 3. 生态系统问题
+- **代理库**: Node.js 生态中缺少完整的代理客户端库
+- **维护性**: 需要自己实现复杂的代理协议
+
+## Go 语言的优势
+
+### 1. 原生代理支持
+```go
+// Go 有成熟的代理库
+import (
+    "golang.org/x/net/proxy"
+    "github.com/Dreamacro/clash/adapter"
+    "github.com/Dreamacro/clash/transport/socks5"
+)
+```
+
+### 2. 高性能并发
+```go
+// 使用 goroutine 处理并发测速
+func testNodes(nodes []Node) {
+    semaphore := make(chan struct{}, 10) // 限制并发数
+    var wg sync.WaitGroup
+    
+    for _, node := range nodes {
+        wg.Add(1)
+        go func(n Node) {
+            defer wg.Done()
+            semaphore <- struct{}{}
+            defer func() { <-semaphore }()
+            
+            testNode(n)
+        }(node)
+    }
+    
+    wg.Wait()
+}
+```
+
+### 3. 更好的网络处理
+```go
+// Go 的网络库更底层,性能更好
+func testLatency(node Node) (int, error) {
+    start := time.Now()
+    
+    conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", node.Server, node.Port), 10*time.Second)
+    if err != nil {
+        return 0, err
+    }
+    defer conn.Close()
+    
+    return int(time.Since(start).Milliseconds()), nil
+}
+```
+
+### 4. 丰富的代理库生态
+- **Clash**: 可以直接使用 Clash 的适配器
+- **Shadowsocks**: 有完整的 Go 实现
+- **VMess**: 有成熟的 VMess 客户端库
+- **Trojan**: 有专门的 Trojan 库
+
+## 推荐的 Go 架构
+
+### 1. 核心组件
+```go
+type SpeedTester struct {
+    nodes     []Node
+    results   chan TestResult
+    config    *Config
+}
+
+type Node struct {
+    Name     string
+    Type     string
+    Server   string
+    Port     int
+    Password string
+    // 其他配置...
+}
+```
+
+### 2. 代理适配器
+```go
+type ProxyAdapter interface {
+    TestLatency() (int, error)
+    TestSpeed() (float64, float64, error)
+    GetIP() (string, error)
+}
+
+// 为每种代理类型实现适配器
+type ShadowsocksAdapter struct {
+    node *Node
+}
+
+type VMessAdapter struct {
+    node *Node
+}
+```
+
+### 3. 并发测速
+```go
+func (st *SpeedTester) TestAll() []TestResult {
+    results := make([]TestResult, 0, len(st.nodes))
+    resultChan := make(chan TestResult, len(st.nodes))
+    
+    // 启动并发测速
+    for _, node := range st.nodes {
+        go func(n Node) {
+            result := st.testNode(n)
+            resultChan <- result
+        }(node)
+    }
+    
+    // 收集结果
+    for i := 0; i < len(st.nodes); i++ {
+        results = append(results, <-resultChan)
+    }
+    
+    return results
+}
+```
+
+## 迁移建议
+
+### 1. 保留现有功能
+- 数据库模型可以复用
+- API 接口保持兼容
+- 前端页面无需修改
+
+### 2. 逐步迁移
+1. 先实现核心测速功能
+2. 添加代理类型支持
+3. 集成数据库
+4. 添加 Web API
+5. 完善前端集成
+
+### 3. 技术栈选择
+- **Web 框架**: Gin 或 Echo
+- **数据库**: 继续使用 MySQL
+- **代理库**: Clash 适配器
+- **配置**: Viper
+- **日志**: Logrus
+
+## 结论
+
+Go 语言确实更适合这个项目,因为:
+1. **协议支持完整**: 有成熟的代理客户端库
+2. **性能更优**: 并发处理能力强
+3. **维护性好**: 代码结构清晰,易于扩展
+4. **生态丰富**: 代理相关库完善
+
+建议采用 Go 重写核心测速功能,保留现有的数据库和前端架构。 

+ 240 - 0
go-speed-test/README.md

@@ -0,0 +1,240 @@
+# Clash 测速工具 (Go版本)
+
+这是一个用Go语言重写的Clash节点测速工具,相比Node.js版本具有更好的性能和更准确的测速结果。
+
+## 主要改进
+
+### 1. 更准确的测速
+- **真实速度测试**: 使用1MB测试文件进行下载和上传速度测试
+- **延迟测试**: 测试节点响应延迟
+- **IP地址检测**: 自动检测节点出口IP地址
+- **地理位置检测**: 识别节点所在地区
+
+### 2. 更完善的代理支持
+- **HTTP/HTTPS代理**: 直接支持
+- **SOCKS5代理**: 直接支持
+- **高级代理**: 通过本地代理客户端(如Clash)支持Shadowsocks、Vmess、Trojan等
+
+### 3. 更好的性能
+- **并发控制**: 可配置的并发测试数量
+- **超时控制**: 可配置的测试超时时间
+- **资源优化**: 更高效的内存和网络使用
+
+## 安装和运行
+
+### 前置要求
+- Go 1.21 或更高版本
+- MySQL 数据库
+
+### 1. 安装Go
+```bash
+# Windows
+# 从 https://golang.org/dl/ 下载并安装
+
+# Linux/Mac
+wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
+sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
+export PATH=$PATH:/usr/local/go/bin
+```
+
+### 2. 配置数据库
+```sql
+CREATE DATABASE clash_speed_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+```
+
+### 3. 配置应用
+编辑 `config.yaml` 文件:
+```yaml
+database:
+  host: "localhost"
+  port: 3306
+  username: "your_username"
+  password: "your_password"
+  database: "clash_speed_test"
+
+speed:
+  test_urls:
+    - "https://www.google.com"
+    - "https://www.youtube.com"
+    - "https://www.github.com"
+  timeout: 10s
+  concurrency: 5
+  interval: 5m
+```
+
+### 4. 运行应用
+
+**Windows:**
+```cmd
+run.bat
+```
+
+**Linux/Mac:**
+```bash
+chmod +x run.sh
+./run.sh
+```
+
+**手动运行:**
+```bash
+go mod tidy
+go build -o clash-speed-test .
+./clash-speed-test
+```
+
+## 功能特性
+
+### 1. 自动测速
+- 定时测试所有活跃节点
+- 可配置测试间隔
+- 支持启动时立即测试
+
+### 2. 详细结果
+- 延迟 (ms)
+- 下载速度 (Mbps)
+- 上传速度 (Mbps)
+- IP地址
+- 地理位置
+- 测试时间
+
+### 3. 错误处理
+- 连接超时处理
+- 代理类型不支持处理
+- 网络错误重试
+
+### 4. 日志记录
+- 详细的测试日志
+- 错误信息记录
+- 性能统计
+
+## API接口
+
+### 获取测试结果
+```bash
+GET /api/results
+```
+
+### 获取节点列表
+```bash
+GET /api/nodes
+```
+
+### 手动触发测试
+```bash
+POST /api/test
+```
+
+## 高级代理测试
+
+对于Shadowsocks、Vmess、Trojan等高级代理,需要:
+
+1. **安装Clash客户端**
+2. **配置代理节点**
+3. **启动HTTP代理服务**
+4. **程序会自动检测本地代理端口**
+
+支持的本地代理端口:
+- 7890 (Clash默认)
+- 7891 (Clash SOCKS)
+- 1080 (通用SOCKS)
+- 8080 (通用HTTP)
+- 8118 (Privoxy)
+
+## 配置说明
+
+### 环境变量
+```bash
+# 数据库配置
+export DB_HOST=localhost
+export DB_PORT=3306
+export DB_USER=root
+export DB_PASS=password
+export DB_NAME=clash_speed_test
+
+# 测速配置
+export SPEED_TEST_TIMEOUT=10s
+export SPEED_TEST_INTERVAL=5m
+export SPEED_TEST_CONCURRENCY=5
+
+# 服务器配置
+export PORT=3000
+```
+
+### 配置文件字段
+- `server.port`: HTTP服务器端口
+- `server.host`: HTTP服务器地址
+- `database.*`: 数据库连接配置
+- `speed.test_urls`: 测试URL列表
+- `speed.timeout`: 测试超时时间
+- `speed.concurrency`: 并发测试数量
+- `speed.interval`: 自动测试间隔
+- `speed.test_on_start`: 启动时是否立即测试
+- `log.level`: 日志级别
+- `log.file`: 日志文件路径
+
+## 故障排除
+
+### 1. 数据库连接失败
+- 检查数据库服务是否启动
+- 验证连接参数是否正确
+- 确认数据库用户权限
+
+### 2. 代理测试失败
+- 检查代理服务器是否可访问
+- 验证代理配置是否正确
+- 确认网络连接正常
+
+### 3. 高级代理无法测试
+- 确保Clash客户端正在运行
+- 检查本地代理端口是否开放
+- 验证代理配置是否正确
+
+## 性能优化
+
+### 1. 调整并发数
+根据网络环境调整 `speed.concurrency` 参数:
+- 网络较好:5-10
+- 网络一般:3-5
+- 网络较差:1-3
+
+### 2. 调整超时时间
+根据节点响应速度调整 `speed.timeout` 参数:
+- 快速节点:5-10秒
+- 一般节点:10-15秒
+- 慢速节点:15-30秒
+
+### 3. 优化测试URL
+选择响应稳定的测试URL:
+- Google: 稳定性好,但可能被屏蔽
+- GitHub: 稳定性好,访问速度快
+- 自定义URL: 根据实际需求选择
+
+## 开发说明
+
+### 项目结构
+```
+go-speed-test/
+├── main.go                 # 主程序入口
+├── config.yaml            # 配置文件
+├── go.mod                 # Go模块文件
+├── go.sum                 # 依赖校验文件
+├── internal/
+│   ├── api/              # API接口
+│   ├── config/           # 配置管理
+│   ├── core/             # 核心逻辑
+│   ├── database/         # 数据库操作
+│   └── logger/           # 日志管理
+├── templates/            # HTML模板
+├── run.bat              # Windows启动脚本
+└── run.sh               # Linux/Mac启动脚本
+```
+
+### 添加新功能
+1. 在 `internal/core/` 中添加核心逻辑
+2. 在 `internal/api/` 中添加API接口
+3. 在 `internal/database/` 中添加数据模型
+4. 更新配置文件支持新参数
+
+## 许可证
+
+MIT License 

+ 284 - 0
go-speed-test/USAGE.md

@@ -0,0 +1,284 @@
+# 使用说明
+
+## 快速开始
+
+### 1. 环境准备
+
+确保您的系统已安装:
+- Go 1.21+ 
+- MySQL 数据库
+- Clash客户端(用于高级代理测试)
+
+### 2. 数据库设置
+
+```sql
+-- 创建数据库
+CREATE DATABASE clash_speed_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- 创建用户(可选)
+CREATE USER 'clash_user'@'localhost' IDENTIFIED BY 'your_password';
+GRANT ALL PRIVILEGES ON clash_speed_test.* TO 'clash_user'@'localhost';
+FLUSH PRIVILEGES;
+```
+
+### 3. 配置应用
+
+编辑 `config.yaml` 文件:
+
+```yaml
+database:
+  host: "localhost"
+  port: 3306
+  username: "root"  # 或您创建的用户
+  password: "your_password"
+  database: "clash_speed_test"
+
+speed:
+  test_urls:
+    - "https://www.google.com"
+    - "https://www.youtube.com"
+    - "https://www.github.com"
+  timeout: 10s
+  concurrency: 5
+  interval: 5m
+```
+
+### 4. 启动应用
+
+**Windows:**
+```cmd
+run.bat
+```
+
+**Linux/Mac:**
+```bash
+chmod +x run.sh
+./run.sh
+```
+
+### 5. 测试功能
+
+运行测试脚本验证功能:
+```bash
+go run test_speed.go
+```
+
+## 功能对比
+
+### Node.js版本的问题
+1. **代理支持不完整**: 试图通过HTTP代理模式测试高级代理,不可靠
+2. **测速精度不够**: 只测试基本连接性,没有真实速度测试
+3. **错误处理不健壮**: 代理创建失败时没有合适的降级策略
+4. **性能问题**: Node.js的异步处理在高并发时可能不稳定
+
+### Go版本的改进
+1. **更准确的测速**: 
+   - 使用1MB测试文件进行真实下载/上传速度测试
+   - 精确的延迟测量
+   - 支持多种测试URL
+
+2. **更完善的代理支持**:
+   - HTTP/HTTPS代理:直接支持
+   - SOCKS5代理:直接支持
+   - 高级代理:通过本地代理客户端支持
+
+3. **更好的性能**:
+   - 可配置的并发控制
+   - 更高效的资源使用
+   - 更稳定的网络处理
+
+4. **更健壮的错误处理**:
+   - 连接超时处理
+   - 代理类型不支持处理
+   - 网络错误重试机制
+
+## 高级配置
+
+### 1. 调整测速参数
+
+```yaml
+speed:
+  timeout: 15s        # 增加超时时间
+  concurrency: 3      # 减少并发数
+  interval: 10m       # 增加测试间隔
+```
+
+### 2. 自定义测试URL
+
+```yaml
+speed:
+  test_urls:
+    - "https://www.google.com"
+    - "https://www.youtube.com"
+    - "https://www.github.com"
+    - "https://httpbin.org/get"    # 添加自定义URL
+    - "https://api.ipify.org"      # IP检测服务
+```
+
+### 3. 环境变量配置
+
+```bash
+# 数据库配置
+export DB_HOST=localhost
+export DB_PORT=3306
+export DB_USER=clash_user
+export DB_PASS=your_password
+export DB_NAME=clash_speed_test
+
+# 测速配置
+export SPEED_TEST_TIMEOUT=15s
+export SPEED_TEST_INTERVAL=10m
+export SPEED_TEST_CONCURRENCY=3
+
+# 服务器配置
+export PORT=3000
+```
+
+## 故障排除
+
+### 1. 数据库连接失败
+
+**错误信息**: `连接数据库失败: dial tcp: connect: connection refused`
+
+**解决方案**:
+```bash
+# 检查MySQL服务状态
+sudo systemctl status mysql
+
+# 启动MySQL服务
+sudo systemctl start mysql
+
+# 检查端口是否开放
+netstat -tlnp | grep 3306
+```
+
+### 2. 代理测试失败
+
+**错误信息**: `延迟测试失败: 所有测试URL都无法访问`
+
+**解决方案**:
+1. 检查代理服务器是否可访问
+2. 验证代理配置是否正确
+3. 确认网络连接正常
+4. 尝试不同的测试URL
+
+### 3. 高级代理无法测试
+
+**错误信息**: `无法通过本地代理测试节点`
+
+**解决方案**:
+1. 确保Clash客户端正在运行
+2. 检查本地代理端口是否开放:
+   ```bash
+   netstat -tlnp | grep 7890
+   netstat -tlnp | grep 7891
+   ```
+3. 验证代理配置是否正确
+4. 尝试手动测试代理连接
+
+### 4. 编译错误
+
+**错误信息**: `go: module lookup disabled by GOPROXY=off`
+
+**解决方案**:
+```bash
+# 设置Go代理
+export GOPROXY=https://goproxy.cn,direct
+
+# 或者使用官方代理
+export GOPROXY=https://proxy.golang.org,direct
+```
+
+## 性能优化建议
+
+### 1. 网络环境优化
+
+- **网络较好**: 并发数5-10,超时时间5-10秒
+- **网络一般**: 并发数3-5,超时时间10-15秒  
+- **网络较差**: 并发数1-3,超时时间15-30秒
+
+### 2. 系统资源优化
+
+- **内存**: 建议至少2GB可用内存
+- **CPU**: 建议至少2核心CPU
+- **网络**: 建议稳定的网络连接
+
+### 3. 数据库优化
+
+```sql
+-- 优化MySQL配置
+SET GLOBAL max_connections = 200;
+SET GLOBAL innodb_buffer_pool_size = 1073741824; -- 1GB
+```
+
+## 监控和维护
+
+### 1. 日志监控
+
+程序会输出详细的日志信息:
+- 测试开始/结束
+- 节点测试结果
+- 错误信息
+- 性能统计
+
+### 2. 数据库维护
+
+定期清理旧的测试数据:
+```sql
+-- 删除30天前的测试结果
+DELETE FROM test_results WHERE test_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
+
+-- 优化表
+OPTIMIZE TABLE test_results;
+```
+
+### 3. 系统监控
+
+监控关键指标:
+- CPU使用率
+- 内存使用率
+- 网络连接数
+- 数据库连接数
+
+## 扩展功能
+
+### 1. 添加新的代理类型
+
+在 `internal/core/speed_tester.go` 中添加新的测试方法:
+
+```go
+func (st *SpeedTester) testNewProxyType(node database.Node) (*SpeedTestResult, error) {
+    // 实现新的代理类型测试逻辑
+}
+```
+
+### 2. 添加新的测试指标
+
+在 `SpeedTestResult` 结构体中添加新字段:
+
+```go
+type SpeedTestResult struct {
+    // 现有字段...
+    NewMetric float64 `json:"new_metric"`
+}
+```
+
+### 3. 自定义测试URL
+
+在配置文件中添加自定义测试URL:
+
+```yaml
+speed:
+  test_urls:
+    - "https://your-custom-test-url.com"
+```
+
+## 技术支持
+
+如果遇到问题,请:
+
+1. 检查日志输出
+2. 验证配置文件
+3. 测试网络连接
+4. 查看故障排除部分
+5. 提交Issue到项目仓库 

+ 25 - 0
go-speed-test/config.yaml

@@ -0,0 +1,25 @@
+server:
+  port: 3000
+  host: "0.0.0.0"
+
+database:
+  host: "localhost"
+  port: 3306
+  username: "root"
+  password: ""
+  database: "clash_speed_test"
+
+speed:
+  test_urls:
+    - "https://www.google.com"
+    - "https://www.youtube.com"
+    - "https://www.github.com"
+    - "https://httpbin.org/get"
+  timeout: 10s
+  concurrency: 5
+  interval: 5m
+  test_on_start: true
+
+log:
+  level: "info"
+  file: "" 

+ 56 - 0
go-speed-test/go.mod

@@ -0,0 +1,56 @@
+module clash-speed-test
+
+go 1.21
+
+require (
+	github.com/gin-gonic/gin v1.9.1
+	github.com/sirupsen/logrus v1.9.3
+	github.com/spf13/viper v1.17.0
+	gorm.io/driver/mysql v1.5.2
+	gorm.io/gorm v1.25.5
+)
+
+require (
+	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.14.0 // indirect
+	github.com/go-sql-driver/mysql v1.7.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/sagikazarmark/locafero v0.3.0 // indirect
+	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.10.0 // indirect
+	github.com/spf13/cast v1.5.1 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.9.0 // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/crypto v0.13.0 // indirect
+	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+	golang.org/x/net v0.15.0 // indirect
+	golang.org/x/sys v0.12.0 // indirect
+	golang.org/x/text v0.13.0 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 571 - 0
go-speed-test/go.sum

@@ -0,0 +1,571 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
+github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
+github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
+github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
+github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
+github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
+gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
+gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
+gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

+ 287 - 0
go-speed-test/internal/api/server.go

@@ -0,0 +1,287 @@
+package api
+
+import (
+	"context"
+	"net/http"
+	"strconv"
+
+	"clash-speed-test/internal/config"
+	"clash-speed-test/internal/core"
+	"clash-speed-test/internal/database"
+	"clash-speed-test/internal/logger"
+
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm"
+)
+
+type Server struct {
+	config      *config.Config
+	db          *gorm.DB
+	speedTester *core.SpeedTester
+	scheduler   *core.Scheduler
+	router      *gin.Engine
+	server      *http.Server
+}
+
+func NewServer(cfg *config.Config, db *gorm.DB, speedTester *core.SpeedTester, scheduler *core.Scheduler) *Server {
+	gin.SetMode(gin.ReleaseMode)
+	router := gin.New()
+	router.Use(gin.Recovery())
+
+	server := &Server{
+		config:      cfg,
+		db:          db,
+		speedTester: speedTester,
+		scheduler:   scheduler,
+		router:      router,
+	}
+
+	server.setupRoutes()
+	return server
+}
+
+func (s *Server) setupRoutes() {
+	// 健康检查
+	s.router.GET("/health", s.healthCheck)
+
+	// API路由组
+	api := s.router.Group("/api")
+	{
+		// 节点管理
+		api.GET("/nodes", s.getNodes)
+		api.POST("/nodes", s.createNode)
+		api.PUT("/nodes/:id", s.updateNode)
+		api.DELETE("/nodes/:id", s.deleteNode)
+
+		// 测速相关
+		api.GET("/speed-test", s.getSpeedTestResults)
+		api.POST("/speed-test/trigger", s.triggerSpeedTest)
+		api.GET("/speed-test/history/:nodeId", s.getNodeTestHistory)
+
+		// 系统状态
+		api.GET("/status", s.getSystemStatus)
+	}
+
+	// 静态文件服务
+	s.router.Static("/static", "./static")
+	s.router.LoadHTMLGlob("templates/*")
+
+	// 主页
+	s.router.GET("/", s.index)
+}
+
+// 启动服务器
+func (s *Server) Start() error {
+	addr := s.config.Server.Host + ":" + strconv.Itoa(s.config.Server.Port)
+	s.server = &http.Server{
+		Addr:    addr,
+		Handler: s.router,
+	}
+
+	return s.server.ListenAndServe()
+}
+
+// 关闭服务器
+func (s *Server) Shutdown(ctx context.Context) error {
+	return s.server.Shutdown(ctx)
+}
+
+// 健康检查
+func (s *Server) healthCheck(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"status":  "ok",
+		"service": "clash-speed-test",
+		"version": "1.0.0",
+	})
+}
+
+// 主页
+func (s *Server) index(c *gin.Context) {
+	c.HTML(http.StatusOK, "index.html", gin.H{
+		"title": "Clash Speed Test",
+	})
+}
+
+// 获取所有节点
+func (s *Server) getNodes(c *gin.Context) {
+	var nodes []database.Node
+	if err := s.db.Find(&nodes).Error; err != nil {
+		logger.Error("获取节点列表失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "获取节点列表失败"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": nodes,
+	})
+}
+
+// 创建节点
+func (s *Server) createNode(c *gin.Context) {
+	var node database.Node
+	if err := c.ShouldBindJSON(&node); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
+		return
+	}
+
+	if err := s.db.Create(&node).Error; err != nil {
+		logger.Error("创建节点失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "创建节点失败"})
+		return
+	}
+
+	c.JSON(http.StatusCreated, gin.H{
+		"data": node,
+	})
+}
+
+// 更新节点
+func (s *Server) updateNode(c *gin.Context) {
+	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "无效的节点ID"})
+		return
+	}
+
+	var node database.Node
+	if err := s.db.First(&node, id).Error; err != nil {
+		c.JSON(http.StatusNotFound, gin.H{"error": "节点不存在"})
+		return
+	}
+
+	var updateData database.Node
+	if err := c.ShouldBindJSON(&updateData); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
+		return
+	}
+
+	if err := s.db.Model(&node).Updates(updateData).Error; err != nil {
+		logger.Error("更新节点失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "更新节点失败"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": node,
+	})
+}
+
+// 删除节点
+func (s *Server) deleteNode(c *gin.Context) {
+	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "无效的节点ID"})
+		return
+	}
+
+	if err := s.db.Delete(&database.Node{}, id).Error; err != nil {
+		logger.Error("删除节点失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "删除节点失败"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"message": "节点删除成功"})
+}
+
+// 获取测速结果
+func (s *Server) getSpeedTestResults(c *gin.Context) {
+	limit := 50
+	if limitStr := c.Query("limit"); limitStr != "" {
+		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
+			limit = l
+		}
+	}
+
+	results, err := database.GetRecentTestResults(limit)
+	if err != nil {
+		logger.Error("获取测速结果失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "获取测速结果失败"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": results,
+	})
+}
+
+// 手动触发测速
+func (s *Server) triggerSpeedTest(c *gin.Context) {
+	s.scheduler.TriggerSpeedTest()
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "测速任务已触发",
+	})
+}
+
+// 获取节点测试历史
+func (s *Server) getNodeTestHistory(c *gin.Context) {
+	nodeID, err := strconv.ParseUint(c.Param("nodeId"), 10, 32)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "无效的节点ID"})
+		return
+	}
+
+	limit := 20
+	if limitStr := c.Query("limit"); limitStr != "" {
+		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
+			limit = l
+		}
+	}
+
+	results, err := database.GetNodeTestHistory(uint(nodeID), limit)
+	if err != nil {
+		logger.Error("获取节点测试历史失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "获取节点测试历史失败"})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": results,
+	})
+}
+
+// 获取系统状态
+func (s *Server) getSystemStatus(c *gin.Context) {
+	// 获取节点统计
+	var totalNodes, activeNodes int64
+	s.db.Model(&database.Node{}).Count(&totalNodes)
+	s.db.Model(&database.Node{}).Where("is_active = ?", true).Count(&activeNodes)
+
+	// 获取测试结果统计
+	var totalTests, successTests int64
+	s.db.Model(&database.TestResult{}).Count(&totalTests)
+	s.db.Model(&database.TestResult{}).Where("is_success = ?", true).Count(&successTests)
+
+	// 计算平均延迟
+	var avgLatency float64
+	s.db.Model(&database.TestResult{}).
+		Where("is_success = ? AND latency IS NOT NULL", true).
+		Select("AVG(latency)").
+		Scan(&avgLatency)
+
+	// 计算成功率
+	var successRate float64
+	if totalTests > 0 {
+		successRate = float64(successTests) / float64(totalTests) * 100
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"total_nodes":      totalNodes,
+		"active_nodes":     activeNodes,
+		"avg_latency":      int(avgLatency),
+		"success_rate":     int(successRate),
+		"scheduler_running": s.scheduler.IsRunning(),
+	})
+} 

+ 139 - 0
go-speed-test/internal/config/config.go

@@ -0,0 +1,139 @@
+package config
+
+import (
+	"os"
+	"strconv"
+	"time"
+
+	"github.com/spf13/viper"
+)
+
+type Config struct {
+	Server   ServerConfig   `mapstructure:"server"`
+	Database DatabaseConfig `mapstructure:"database"`
+	Speed    SpeedConfig    `mapstructure:"speed"`
+	Log      LogConfig      `mapstructure:"log"`
+}
+
+type ServerConfig struct {
+	Port int    `mapstructure:"port"`
+	Host string `mapstructure:"host"`
+}
+
+type DatabaseConfig struct {
+	Host     string `mapstructure:"host"`
+	Port     int    `mapstructure:"port"`
+	Username string `mapstructure:"username"`
+	Password string `mapstructure:"password"`
+	Database string `mapstructure:"database"`
+}
+
+type SpeedConfig struct {
+	TestURLs     []string      `mapstructure:"test_urls"`
+	Timeout      time.Duration `mapstructure:"timeout"`
+	Concurrency  int           `mapstructure:"concurrency"`
+	Interval     time.Duration `mapstructure:"interval"`
+	TestOnStart  bool          `mapstructure:"test_on_start"`
+}
+
+type LogConfig struct {
+	Level string `mapstructure:"level"`
+	File  string `mapstructure:"file"`
+}
+
+func Load() (*Config, error) {
+	// 设置默认值
+	setDefaults()
+
+	// 读取环境变量
+	viper.AutomaticEnv()
+
+	// 读取配置文件
+	viper.SetConfigName("config")
+	viper.SetConfigType("yaml")
+	viper.AddConfigPath(".")
+	viper.AddConfigPath("./config")
+
+	if err := viper.ReadInConfig(); err != nil {
+		// 如果配置文件不存在,使用默认值
+		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
+			return nil, err
+		}
+	}
+
+	var config Config
+	if err := viper.Unmarshal(&config); err != nil {
+		return nil, err
+	}
+
+	return &config, nil
+}
+
+func setDefaults() {
+	// 服务器配置
+	viper.SetDefault("server.port", 3000)
+	viper.SetDefault("server.host", "0.0.0.0")
+
+	// 数据库配置
+	viper.SetDefault("database.host", "localhost")
+	viper.SetDefault("database.port", 3306)
+	viper.SetDefault("database.username", "root")
+	viper.SetDefault("database.password", "")
+	viper.SetDefault("database.database", "clash_speed_test")
+
+	// 测速配置
+	viper.SetDefault("speed.test_urls", []string{
+		"https://www.google.com",
+		"https://www.youtube.com",
+		"https://www.github.com",
+	})
+	viper.SetDefault("speed.timeout", 10*time.Second)
+	viper.SetDefault("speed.concurrency", 5)
+	viper.SetDefault("speed.interval", 5*time.Minute)
+	viper.SetDefault("speed.test_on_start", true)
+
+	// 日志配置
+	viper.SetDefault("log.level", "info")
+	viper.SetDefault("log.file", "")
+
+	// 环境变量覆盖
+	if port := os.Getenv("PORT"); port != "" {
+		if p, err := strconv.Atoi(port); err == nil {
+			viper.Set("server.port", p)
+		}
+	}
+
+	if dbHost := os.Getenv("DB_HOST"); dbHost != "" {
+		viper.Set("database.host", dbHost)
+	}
+
+	if dbPort := os.Getenv("DB_PORT"); dbPort != "" {
+		if p, err := strconv.Atoi(dbPort); err == nil {
+			viper.Set("database.port", p)
+		}
+	}
+
+	if dbUser := os.Getenv("DB_USER"); dbUser != "" {
+		viper.Set("database.username", dbUser)
+	}
+
+	if dbPass := os.Getenv("DB_PASS"); dbPass != "" {
+		viper.Set("database.password", dbPass)
+	}
+
+	if dbName := os.Getenv("DB_NAME"); dbName != "" {
+		viper.Set("database.database", dbName)
+	}
+
+	if interval := os.Getenv("SPEED_TEST_INTERVAL"); interval != "" {
+		if d, err := time.ParseDuration(interval); err == nil {
+			viper.Set("speed.interval", d)
+		}
+	}
+
+	if timeout := os.Getenv("SPEED_TEST_TIMEOUT"); timeout != "" {
+		if d, err := time.ParseDuration(timeout); err == nil {
+			viper.Set("speed.timeout", d)
+		}
+	}
+} 

+ 143 - 0
go-speed-test/internal/core/scheduler.go

@@ -0,0 +1,143 @@
+package core
+
+import (
+	"sync"
+	"time"
+
+	"clash-speed-test/internal/config"
+	"clash-speed-test/internal/database"
+	"clash-speed-test/internal/logger"
+)
+
+type Scheduler struct {
+	speedTester *SpeedTester
+	config      *config.Config
+	ticker      *time.Ticker
+	stopChan    chan struct{}
+	running     bool
+	mu          sync.RWMutex
+}
+
+func NewScheduler(speedTester *SpeedTester, cfg *config.Config) *Scheduler {
+	return &Scheduler{
+		speedTester: speedTester,
+		config:      cfg,
+		stopChan:    make(chan struct{}),
+	}
+}
+
+// 启动调度器
+func (s *Scheduler) Start() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if s.running {
+		logger.Warn("调度器已在运行", nil)
+		return
+	}
+
+	s.running = true
+	s.ticker = time.NewTicker(s.config.Speed.Interval)
+
+	logger.Info("启动定时任务调度器", map[string]interface{}{
+		"interval": s.config.Speed.Interval,
+	})
+
+	// 如果配置了启动时立即测试
+	if s.config.Speed.TestOnStart {
+		go s.runSpeedTest()
+	}
+
+	// 启动定时任务
+	go func() {
+		for {
+			select {
+			case <-s.ticker.C:
+				s.runSpeedTest()
+			case <-s.stopChan:
+				return
+			}
+		}
+	}()
+}
+
+// 停止调度器
+func (s *Scheduler) Stop() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if !s.running {
+		return
+	}
+
+	s.running = false
+	if s.ticker != nil {
+		s.ticker.Stop()
+	}
+	close(s.stopChan)
+
+	logger.Info("定时任务调度器已停止", nil)
+}
+
+// 手动触发测速
+func (s *Scheduler) TriggerSpeedTest() {
+	s.mu.RLock()
+	if !s.running {
+		s.mu.RUnlock()
+		logger.Warn("调度器未运行,无法触发测速", nil)
+		return
+	}
+	s.mu.RUnlock()
+
+	go s.runSpeedTest()
+}
+
+// 运行测速任务
+func (s *Scheduler) runSpeedTest() {
+	logger.Info("开始执行定时测速任务", nil)
+
+	// 获取所有活跃节点
+	nodes, err := database.GetActiveNodes()
+	if err != nil {
+		logger.Error("获取活跃节点失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	if len(nodes) == 0 {
+		logger.Warn("没有找到活跃节点", nil)
+		return
+	}
+
+	logger.Info("开始测试节点", map[string]interface{}{
+		"count": len(nodes),
+	})
+
+	// 执行测速
+	results := s.speedTester.TestNodes(nodes)
+
+	// 统计结果
+	successCount := 0
+	failCount := 0
+	for _, result := range results {
+		if result.IsSuccess {
+			successCount++
+		} else {
+			failCount++
+		}
+	}
+
+	logger.Info("测速任务完成", map[string]interface{}{
+		"total":   len(results),
+		"success": successCount,
+		"failed":  failCount,
+	})
+}
+
+// 获取调度器状态
+func (s *Scheduler) IsRunning() bool {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	return s.running
+} 

+ 492 - 0
go-speed-test/internal/core/speed_tester.go

@@ -0,0 +1,492 @@
+package core
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"clash-speed-test/internal/config"
+	"clash-speed-test/internal/database"
+	applogger "clash-speed-test/internal/logger"
+)
+
+type SpeedTester struct {
+	config      *config.Config
+	testURLs    []string
+	timeout     time.Duration
+	concurrency int
+	httpClient  *http.Client
+}
+
+type SpeedTestResult struct {
+	Latency       int     `json:"latency"`
+	DownloadSpeed float64 `json:"download_speed"` // Mbps
+	UploadSpeed   float64 `json:"upload_speed"`   // Mbps
+	IPAddress     string  `json:"ip_address"`
+	Location      string  `json:"location"`
+	Success       bool    `json:"success"`
+	ErrorMessage  string  `json:"error_message"`
+}
+
+func NewSpeedTester(cfg *config.Config) *SpeedTester {
+	// 创建HTTP客户端
+	httpClient := &http.Client{
+		Timeout: cfg.Speed.Timeout,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: false,
+			},
+			DisableKeepAlives: true,
+			MaxIdleConns:      100,
+			IdleConnTimeout:   30 * time.Second,
+		},
+	}
+
+	return &SpeedTester{
+		config:      cfg,
+		testURLs:    cfg.Speed.TestURLs,
+		timeout:     cfg.Speed.Timeout,
+		concurrency: cfg.Speed.Concurrency,
+		httpClient:  httpClient,
+	}
+}
+
+// 测试单个节点
+func (st *SpeedTester) TestNode(node database.Node) (*database.TestResult, error) {
+	startTime := time.Now()
+	
+	result := &database.TestResult{
+		NodeID:    node.ID,
+		TestTime:  time.Now(),
+		IsSuccess: false,
+	}
+
+	applogger.Info("开始测试节点", map[string]interface{}{
+		"name": node.Name,
+		"type": node.Type,
+		"server": node.Server,
+		"port": node.Port,
+	})
+
+	// 测试连接性和速度
+	speedResult, err := st.testNodeSpeed(node)
+	if err != nil {
+		result.ErrorMessage = fmt.Sprintf("测速失败: %v", err)
+		applogger.Warn("节点测试失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": err.Error(),
+		})
+	} else if speedResult.Success {
+		result.IsSuccess = true
+		result.Latency = &speedResult.Latency
+		result.IPAddress = speedResult.IPAddress
+		result.Location = speedResult.Location
+		result.TestURL = st.testURLs[0]
+
+		// 如果有速度数据,保存到扩展字段
+		if speedResult.DownloadSpeed > 0 {
+			downloadSpeed := int(speedResult.DownloadSpeed)
+			result.DownloadSpeed = &downloadSpeed
+		}
+		if speedResult.UploadSpeed > 0 {
+			uploadSpeed := int(speedResult.UploadSpeed)
+			result.UploadSpeed = &uploadSpeed
+		}
+
+		applogger.Info("节点测试成功", map[string]interface{}{
+			"node":           node.Name,
+			"latency":        speedResult.Latency,
+			"download_speed": speedResult.DownloadSpeed,
+			"upload_speed":   speedResult.UploadSpeed,
+			"ip":             speedResult.IPAddress,
+			"location":       speedResult.Location,
+		})
+	} else {
+		result.ErrorMessage = speedResult.ErrorMessage
+		applogger.Warn("节点测试失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": speedResult.ErrorMessage,
+		})
+	}
+
+	// 计算测试时长
+	duration := int(time.Since(startTime).Milliseconds())
+	result.TestDuration = &duration
+
+	// 保存测试结果
+	if err := database.SaveTestResult(result); err != nil {
+		applogger.Error("保存测试结果失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": err.Error(),
+		})
+	}
+
+	return result, nil
+}
+
+// 批量测试节点
+func (st *SpeedTester) TestNodes(nodes []database.Node) []*database.TestResult {
+	var results []*database.TestResult
+	var mu sync.Mutex
+	var wg sync.WaitGroup
+
+	// 创建信号量控制并发数
+	semaphore := make(chan struct{}, st.concurrency)
+
+	for _, node := range nodes {
+		wg.Add(1)
+		go func(n database.Node) {
+			defer wg.Done()
+			
+			// 获取信号量
+			semaphore <- struct{}{}
+			defer func() { <-semaphore }()
+
+			result, err := st.TestNode(n)
+			if err != nil {
+							applogger.Error("节点测试异常", map[string]interface{}{
+				"node":  n.Name,
+				"error": err.Error(),
+			})
+				return
+			}
+
+			mu.Lock()
+			results = append(results, result)
+			mu.Unlock()
+		}(node)
+	}
+
+	wg.Wait()
+	return results
+}
+
+// 测试节点速度
+func (st *SpeedTester) testNodeSpeed(node database.Node) (*SpeedTestResult, error) {
+	result := &SpeedTestResult{
+		Success: false,
+	}
+
+	// 根据代理类型选择测试策略
+	switch node.Type {
+	case "http", "https":
+		return st.testHTTPProxy(node)
+	case "socks5":
+		return st.testSOCKS5Proxy(node)
+	case "ss", "ssr", "vmess", "trojan":
+		// 对于高级代理,尝试多种测试方法
+		return st.testAdvancedProxy(node)
+	default:
+		return result, fmt.Errorf("暂不支持代理类型: %s", node.Type)
+	}
+}
+
+// 测试HTTP代理
+func (st *SpeedTester) testHTTPProxy(node database.Node) (*SpeedTestResult, error) {
+	result := &SpeedTestResult{}
+
+	// 构建代理URL
+	proxyURL := fmt.Sprintf("http://%s:%d", node.Server, node.Port)
+	if node.Username != "" && node.Password != "" {
+		proxyURL = fmt.Sprintf("http://%s:%s@%s:%d", node.Username, node.Password, node.Server, node.Port)
+	}
+
+	// 创建代理客户端
+	proxyURLParsed, err := url.Parse(proxyURL)
+	if err != nil {
+		return result, fmt.Errorf("解析代理URL失败: %w", err)
+	}
+
+	proxyClient := &http.Client{
+		Timeout: st.timeout,
+		Transport: &http.Transport{
+			Proxy: http.ProxyURL(proxyURLParsed),
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: false,
+			},
+			DisableKeepAlives: true,
+		},
+	}
+
+	// 测试延迟
+	latency, ipAddress, location, err := st.testLatency(proxyClient)
+	if err != nil {
+		return result, fmt.Errorf("延迟测试失败: %w", err)
+	}
+
+	result.Latency = latency
+	result.IPAddress = ipAddress
+	result.Location = location
+
+	// 测试下载速度
+	downloadSpeed, err := st.testDownloadSpeed(proxyClient)
+	if err != nil {
+		applogger.Warn("下载速度测试失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": err.Error(),
+		})
+	} else {
+		result.DownloadSpeed = downloadSpeed
+	}
+
+	// 测试上传速度
+	uploadSpeed, err := st.testUploadSpeed(proxyClient)
+	if err != nil {
+		applogger.Warn("上传速度测试失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": err.Error(),
+		})
+	} else {
+		result.UploadSpeed = uploadSpeed
+	}
+
+	result.Success = true
+	return result, nil
+}
+
+// 测试SOCKS5代理
+func (st *SpeedTester) testSOCKS5Proxy(node database.Node) (*SpeedTestResult, error) {
+	result := &SpeedTestResult{}
+
+	// 构建SOCKS5代理URL
+	proxyURL := fmt.Sprintf("socks5://%s:%d", node.Server, node.Port)
+	if node.Username != "" && node.Password != "" {
+		proxyURL = fmt.Sprintf("socks5://%s:%s@%s:%d", node.Username, node.Password, node.Server, node.Port)
+	}
+
+	// 创建代理客户端
+	proxyURLParsed, err := url.Parse(proxyURL)
+	if err != nil {
+		return result, fmt.Errorf("解析SOCKS5代理URL失败: %w", err)
+	}
+
+	proxyClient := &http.Client{
+		Timeout: st.timeout,
+		Transport: &http.Transport{
+			Proxy: http.ProxyURL(proxyURLParsed),
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: false,
+			},
+			DisableKeepAlives: true,
+		},
+	}
+
+	// 测试延迟
+	latency, ipAddress, location, err := st.testLatency(proxyClient)
+	if err != nil {
+		return result, fmt.Errorf("延迟测试失败: %w", err)
+	}
+
+	result.Latency = latency
+	result.IPAddress = ipAddress
+	result.Location = location
+
+	// 测试下载速度
+	downloadSpeed, err := st.testDownloadSpeed(proxyClient)
+	if err != nil {
+		applogger.Warn("下载速度测试失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": err.Error(),
+		})
+	} else {
+		result.DownloadSpeed = downloadSpeed
+	}
+
+	// 测试上传速度
+	uploadSpeed, err := st.testUploadSpeed(proxyClient)
+	if err != nil {
+		applogger.Warn("上传速度测试失败", map[string]interface{}{
+			"node":  node.Name,
+			"error": err.Error(),
+		})
+	} else {
+		result.UploadSpeed = uploadSpeed
+	}
+
+	result.Success = true
+	return result, nil
+}
+
+// 测试高级代理(Shadowsocks、Vmess等)
+func (st *SpeedTester) testAdvancedProxy(node database.Node) (*SpeedTestResult, error) {
+	result := &SpeedTestResult{}
+
+	// 对于高级代理,我们尝试通过本地代理端口进行测试
+	// 这需要代理客户端(如Clash)在本地运行并提供HTTP代理服务
+
+	// 尝试常见的本地代理端口
+	localProxyPorts := []int{7890, 7891, 1080, 8080, 8118}
+	
+	for _, port := range localProxyPorts {
+		proxyURL := fmt.Sprintf("http://127.0.0.1:%d", port)
+		proxyURLParsed, err := url.Parse(proxyURL)
+		if err != nil {
+			continue
+		}
+
+		proxyClient := &http.Client{
+			Timeout: st.timeout,
+			Transport: &http.Transport{
+				Proxy: http.ProxyURL(proxyURLParsed),
+				TLSClientConfig: &tls.Config{
+					InsecureSkipVerify: false,
+				},
+				DisableKeepAlives: true,
+			},
+		}
+
+		// 测试延迟
+		latency, ipAddress, location, err := st.testLatency(proxyClient)
+		if err == nil {
+			result.Latency = latency
+			result.IPAddress = ipAddress
+			result.Location = location
+			result.Success = true
+
+			// 测试下载速度
+			if downloadSpeed, err := st.testDownloadSpeed(proxyClient); err == nil {
+				result.DownloadSpeed = downloadSpeed
+			}
+
+			// 测试上传速度
+			if uploadSpeed, err := st.testUploadSpeed(proxyClient); err == nil {
+				result.UploadSpeed = uploadSpeed
+			}
+
+			logger.Info("通过本地代理测试成功", map[string]interface{}{
+				"node": node.Name,
+				"port": port,
+			})
+
+			return result, nil
+		}
+	}
+
+	return result, fmt.Errorf("无法通过本地代理测试节点: %s", node.Name)
+}
+
+// 测试延迟
+func (st *SpeedTester) testLatency(client *http.Client) (int, string, string, error) {
+	startTime := time.Now()
+
+	// 尝试多个测试URL
+	for _, testURL := range st.testURLs {
+		req, err := http.NewRequest("HEAD", testURL, nil)
+		if err != nil {
+			continue
+		}
+
+		req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+
+		resp, err := client.Do(req)
+		if err != nil {
+			continue
+		}
+		defer resp.Body.Close()
+
+		latency := int(time.Since(startTime).Milliseconds())
+		ipAddress := st.extractIPAddress(resp)
+		location := st.extractLocation(resp)
+
+		return latency, ipAddress, location, nil
+	}
+
+	return 0, "", "", fmt.Errorf("所有测试URL都无法访问")
+}
+
+// 测试下载速度
+func (st *SpeedTester) testDownloadSpeed(client *http.Client) (float64, error) {
+	// 使用1MB的测试文件
+	testURL := "https://httpbin.org/bytes/1048576"
+	
+	startTime := time.Now()
+	
+	req, err := http.NewRequest("GET", testURL, nil)
+	if err != nil {
+		return 0, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return 0, fmt.Errorf("下载测试失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 读取响应体
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return 0, fmt.Errorf("读取响应失败: %w", err)
+	}
+
+	duration := time.Since(startTime).Seconds()
+	fileSize := len(body)
+
+	// 计算下载速度 (Mbps)
+	speedBps := float64(fileSize) / duration
+	speedMbps := (speedBps * 8) / (1024 * 1024)
+
+	return speedMbps, nil
+}
+
+// 测试上传速度
+func (st *SpeedTester) testUploadSpeed(client *http.Client) (float64, error) {
+	// 使用1MB的测试数据
+	testData := strings.Repeat("A", 1024*1024)
+	
+	startTime := time.Now()
+	
+	req, err := http.NewRequest("POST", "https://httpbin.org/post", strings.NewReader(testData))
+	if err != nil {
+		return 0, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "application/octet-stream")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return 0, fmt.Errorf("上传测试失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	duration := time.Since(startTime).Seconds()
+	fileSize := len(testData)
+
+	// 计算上传速度 (Mbps)
+	speedBps := float64(fileSize) / duration
+	speedMbps := (speedBps * 8) / (1024 * 1024)
+
+	return speedMbps, nil
+}
+
+// 提取IP地址
+func (st *SpeedTester) extractIPAddress(resp *http.Response) string {
+	if ip := resp.Header.Get("X-Forwarded-For"); ip != "" {
+		return strings.Split(ip, ",")[0]
+	}
+	if ip := resp.Header.Get("X-Real-IP"); ip != "" {
+		return ip
+	}
+	if ip := resp.Header.Get("CF-Connecting-IP"); ip != "" {
+		return ip
+	}
+	return "unknown"
+}
+
+// 提取位置信息
+func (st *SpeedTester) extractLocation(resp *http.Response) string {
+	if resp.Header.Get("CF-Ray") != "" {
+		return "Cloudflare"
+	}
+	if country := resp.Header.Get("CF-IPCountry"); country != "" {
+		return country
+	}
+	return "unknown"
+} 

+ 136 - 0
go-speed-test/internal/database/database.go

@@ -0,0 +1,136 @@
+package database
+
+import (
+	"fmt"
+	"time"
+
+	"clash-speed-test/internal/config"
+	applogger "clash-speed-test/internal/logger"
+
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	gormlogger "gorm.io/gorm/logger"
+)
+
+var DB *gorm.DB
+
+type Node struct {
+	ID          uint      `gorm:"primarykey" json:"id"`
+	Name        string    `gorm:"size:255;not null" json:"name"`
+	Type        string    `gorm:"size:50;not null" json:"type"`
+	Server      string    `gorm:"size:255;not null" json:"server"`
+	Port        int       `gorm:"not null" json:"port"`
+	Password    string    `gorm:"size:255" json:"password"`
+	Method      string    `gorm:"size:50" json:"method"`
+	UUID        string    `gorm:"size:255" json:"uuid"`
+	AlterID     int       `json:"alter_id"`
+	Network     string    `gorm:"size:50" json:"network"`
+	TLS         bool      `json:"tls"`
+	SNI         string    `gorm:"size:255" json:"sni"`
+	WSPath      string    `gorm:"size:255" json:"ws_path"`
+	WSHeaders   string    `gorm:"type:text" json:"ws_headers"`
+	Username    string    `gorm:"size:255" json:"username"`
+	Protocol    string    `gorm:"size:50" json:"protocol"`
+	Obfs        string    `gorm:"size:50" json:"obfs"`
+	Group       string    `gorm:"size:255" json:"group"`
+	IsActive    bool      `gorm:"default:true" json:"is_active"`
+	CreatedAt   time.Time `json:"created_at"`
+	UpdatedAt   time.Time `json:"updated_at"`
+}
+
+type TestResult struct {
+	ID           uint      `gorm:"primarykey" json:"id"`
+	NodeID       uint      `gorm:"not null" json:"node_id"`
+	Node         Node      `gorm:"foreignKey:NodeID" json:"node"`
+	TestTime     time.Time `gorm:"not null" json:"test_time"`
+	IsSuccess    bool      `gorm:"not null" json:"is_success"`
+	Latency      *int      `json:"latency"`
+	DownloadSpeed *float64 `json:"download_speed"`
+	UploadSpeed   *float64 `json:"upload_speed"`
+	PacketLoss    *float64 `json:"packet_loss"`
+	TestURL       string   `gorm:"size:500" json:"test_url"`
+	ErrorMessage  string   `gorm:"type:text" json:"error_message"`
+	TestDuration  *int     `json:"test_duration"`
+	IPAddress     string   `gorm:"size:100" json:"ip_address"`
+	Location      string   `gorm:"size:255" json:"location"`
+	CreatedAt     time.Time `json:"created_at"`
+	UpdatedAt     time.Time `json:"updated_at"`
+}
+
+type Notification struct {
+	ID        uint      `gorm:"primarykey" json:"id"`
+	Type      string    `gorm:"size:50;not null" json:"type"`
+	Title     string    `gorm:"size:255;not null" json:"title"`
+	Message   string    `gorm:"type:text;not null" json:"message"`
+	IsRead    bool      `gorm:"default:false" json:"is_read"`
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
+}
+
+func Init(cfg config.DatabaseConfig) (*gorm.DB, error) {
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+		cfg.Username,
+		cfg.Password,
+		cfg.Host,
+		cfg.Port,
+		cfg.Database,
+	)
+
+	// 配置GORM日志
+	gormLogger := gormlogger.New(
+		&gormlogger.Writer{},
+		gormlogger.Config{
+			SlowThreshold:             time.Second,
+			LogLevel:                  gormlogger.Warn,
+			IgnoreRecordNotFoundError: true,
+			Colorful:                  false,
+		},
+	)
+
+	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
+		Logger: gormLogger,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("连接数据库失败: %w", err)
+	}
+
+	// 自动迁移表结构
+	if err := db.AutoMigrate(&Node{}, &TestResult{}, &Notification{}); err != nil {
+		return nil, fmt.Errorf("数据库迁移失败: %w", err)
+	}
+
+	DB = db
+	applogger.Info("数据库连接成功", map[string]interface{}{
+		"host": cfg.Host,
+		"port": cfg.Port,
+		"database": cfg.Database,
+	})
+
+	return db, nil
+}
+
+// 获取所有活跃节点
+func GetActiveNodes() ([]Node, error) {
+	var nodes []Node
+	err := DB.Where("is_active = ?", true).Find(&nodes).Error
+	return nodes, err
+}
+
+// 保存测试结果
+func SaveTestResult(result *TestResult) error {
+	return DB.Create(result).Error
+}
+
+// 获取最近的测试结果
+func GetRecentTestResults(limit int) ([]TestResult, error) {
+	var results []TestResult
+	err := DB.Preload("Node").Order("test_time DESC").Limit(limit).Find(&results).Error
+	return results, err
+}
+
+// 获取节点的测试历史
+func GetNodeTestHistory(nodeID uint, limit int) ([]TestResult, error) {
+	var results []TestResult
+	err := DB.Where("node_id = ?", nodeID).Order("test_time DESC").Limit(limit).Find(&results).Error
+	return results, err
+} 

+ 85 - 0
go-speed-test/internal/logger/logger.go

@@ -0,0 +1,85 @@
+package logger
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+)
+
+var log *logrus.Logger
+
+func Init() {
+	log = logrus.New()
+	
+	// 设置日志格式
+	log.SetFormatter(&logrus.JSONFormatter{
+		TimestampFormat: "2006-01-02 15:04:05",
+		FieldMap: logrus.FieldMap{
+			logrus.FieldKeyTime:  "timestamp",
+			logrus.FieldKeyLevel: "level",
+			logrus.FieldKeyMsg:   "message",
+		},
+	})
+
+	// 设置日志级别
+	level := os.Getenv("LOG_LEVEL")
+	if level == "" {
+		level = "info"
+	}
+
+	switch level {
+	case "debug":
+		log.SetLevel(logrus.DebugLevel)
+	case "info":
+		log.SetLevel(logrus.InfoLevel)
+	case "warn":
+		log.SetLevel(logrus.WarnLevel)
+	case "error":
+		log.SetLevel(logrus.ErrorLevel)
+	default:
+		log.SetLevel(logrus.InfoLevel)
+	}
+
+	// 设置输出
+	log.SetOutput(os.Stdout)
+}
+
+func Debug(msg string, fields map[string]interface{}) {
+	if fields == nil {
+		fields = make(map[string]interface{})
+	}
+	fields["service"] = "clash-speed-test"
+	log.WithFields(fields).Debug(msg)
+}
+
+func Info(msg string, fields map[string]interface{}) {
+	if fields == nil {
+		fields = make(map[string]interface{})
+	}
+	fields["service"] = "clash-speed-test"
+	log.WithFields(fields).Info(msg)
+}
+
+func Warn(msg string, fields map[string]interface{}) {
+	if fields == nil {
+		fields = make(map[string]interface{})
+	}
+	fields["service"] = "clash-speed-test"
+	log.WithFields(fields).Warn(msg)
+}
+
+func Error(msg string, fields map[string]interface{}) {
+	if fields == nil {
+		fields = make(map[string]interface{})
+	}
+	fields["service"] = "clash-speed-test"
+	log.WithFields(fields).Error(msg)
+}
+
+func Fatal(msg string, fields map[string]interface{}) {
+	if fields == nil {
+		fields = make(map[string]interface{})
+	}
+	fields["service"] = "clash-speed-test"
+	log.WithFields(fields).Fatal(msg)
+} 

+ 82 - 0
go-speed-test/main.go

@@ -0,0 +1,82 @@
+package main
+
+import (
+	"context"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"clash-speed-test/internal/api"
+	"clash-speed-test/internal/config"
+	"clash-speed-test/internal/core"
+	"clash-speed-test/internal/database"
+	applogger "clash-speed-test/internal/logger"
+)
+
+func main() {
+	// 初始化日志
+	applogger.Init()
+
+	// 加载配置
+	cfg, err := config.Load()
+	if err != nil {
+		log.Fatalf("加载配置失败: %v", err)
+	}
+
+	// 初始化数据库
+	db, err := database.Init(cfg.Database)
+	if err != nil {
+		log.Fatalf("初始化数据库失败: %v", err)
+	}
+
+	// 初始化测速器
+	speedTester := core.NewSpeedTester(cfg)
+
+	// 初始化调度器
+	scheduler := core.NewScheduler(speedTester, cfg)
+
+	// 初始化API服务器
+	server := api.NewServer(cfg, db, speedTester, scheduler)
+
+	// 启动调度器
+	go scheduler.Start()
+
+	// 启动HTTP服务器
+	go func() {
+		applogger.Info("启动HTTP服务器", map[string]interface{}{
+			"port": cfg.Server.Port,
+		})
+
+		if err := server.Start(); err != nil && err != http.ErrServerClosed {
+			applogger.Error("HTTP服务器启动失败", map[string]interface{}{
+				"error": err.Error(),
+			})
+		}
+	}()
+
+	// 等待中断信号
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+	<-quit
+
+	applogger.Info("收到关闭信号,开始优雅关闭")
+
+	// 创建超时上下文
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// 停止调度器
+	scheduler.Stop()
+
+	// 关闭HTTP服务器
+	if err := server.Shutdown(ctx); err != nil {
+		applogger.Error("HTTP服务器关闭失败", map[string]interface{}{
+			"error": err.Error(),
+		})
+	}
+
+	applogger.Info("应用已关闭")
+} 

+ 24 - 0
go-speed-test/run.bat

@@ -0,0 +1,24 @@
+@echo off
+echo 启动Clash测速服务...
+
+REM 检查Go是否安装
+go version >nul 2>&1
+if errorlevel 1 (
+    echo 错误: 未找到Go,请先安装Go语言环境
+    pause
+    exit /b 1
+)
+
+REM 安装依赖
+echo 安装依赖...
+go mod tidy
+
+REM 编译项目
+echo 编译项目...
+go build -o clash-speed-test.exe .
+
+REM 运行程序
+echo 启动服务...
+clash-speed-test.exe
+
+pause 

+ 21 - 0
go-speed-test/run.sh

@@ -0,0 +1,21 @@
+#!/bin/bash
+
+echo "启动Clash测速服务..."
+
+# 检查Go是否安装
+if ! command -v go &> /dev/null; then
+    echo "错误: 未找到Go,请先安装Go语言环境"
+    exit 1
+fi
+
+# 安装依赖
+echo "安装依赖..."
+go mod tidy
+
+# 编译项目
+echo "编译项目..."
+go build -o clash-speed-test .
+
+# 运行程序
+echo "启动服务..."
+./clash-speed-test 

+ 225 - 0
go-speed-test/templates/index.html

@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{.title}}</title>
+    <style>
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            margin: 0;
+            padding: 20px;
+            background-color: #f5f5f5;
+        }
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+            overflow: hidden;
+        }
+        .header {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            padding: 20px;
+            text-align: center;
+        }
+        .content {
+            padding: 20px;
+        }
+        .stats {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+            gap: 20px;
+            margin-bottom: 30px;
+        }
+        .stat-card {
+            background: #f8f9fa;
+            padding: 20px;
+            border-radius: 8px;
+            text-align: center;
+            border-left: 4px solid #667eea;
+        }
+        .stat-number {
+            font-size: 2em;
+            font-weight: bold;
+            color: #667eea;
+        }
+        .stat-label {
+            color: #666;
+            margin-top: 5px;
+        }
+        .section {
+            margin-bottom: 30px;
+        }
+        .section h3 {
+            color: #333;
+            border-bottom: 2px solid #667eea;
+            padding-bottom: 10px;
+        }
+        .btn {
+            background: #667eea;
+            color: white;
+            border: none;
+            padding: 10px 20px;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+        }
+        .btn:hover {
+            background: #5a6fd8;
+        }
+        .table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-top: 10px;
+        }
+        .table th, .table td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #ddd;
+        }
+        .table th {
+            background-color: #f8f9fa;
+            font-weight: 600;
+        }
+        .status-success {
+            color: #28a745;
+        }
+        .status-error {
+            color: #dc3545;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="header">
+            <h1>{{.title}}</h1>
+            <p>代理节点测速管理系统</p>
+        </div>
+        
+        <div class="content">
+            <div class="stats">
+                <div class="stat-card">
+                    <div class="stat-number" id="totalNodes">-</div>
+                    <div class="stat-label">总节点数</div>
+                </div>
+                <div class="stat-card">
+                    <div class="stat-number" id="activeNodes">-</div>
+                    <div class="stat-label">活跃节点</div>
+                </div>
+                <div class="stat-card">
+                    <div class="stat-number" id="avgLatency">-</div>
+                    <div class="stat-label">平均延迟(ms)</div>
+                </div>
+                <div class="stat-card">
+                    <div class="stat-number" id="successRate">-</div>
+                    <div class="stat-label">成功率(%)</div>
+                </div>
+            </div>
+
+            <div class="section">
+                <h3>系统状态</h3>
+                <button class="btn" onclick="triggerSpeedTest()">立即测速</button>
+                <button class="btn" onclick="refreshData()">刷新数据</button>
+                <div id="systemStatus">加载中...</div>
+            </div>
+
+            <div class="section">
+                <h3>最近测试结果</h3>
+                <div id="recentResults">加载中...</div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        // 页面加载时获取数据
+        document.addEventListener('DOMContentLoaded', function() {
+            refreshData();
+        });
+
+        // 刷新数据
+        function refreshData() {
+            fetchSystemStatus();
+            fetchRecentResults();
+        }
+
+        // 获取系统状态
+        function fetchSystemStatus() {
+            fetch('/api/status')
+                .then(response => response.json())
+                .then(data => {
+                    document.getElementById('totalNodes').textContent = data.total_nodes || 0;
+                    document.getElementById('activeNodes').textContent = data.active_nodes || 0;
+                    document.getElementById('avgLatency').textContent = data.avg_latency || '-';
+                    document.getElementById('successRate').textContent = data.success_rate || '-';
+                    
+                    const statusDiv = document.getElementById('systemStatus');
+                    statusDiv.innerHTML = `
+                        <p><strong>调度器状态:</strong> ${data.scheduler_running ? '运行中' : '已停止'}</p>
+                        <p><strong>最后更新:</strong> ${new Date().toLocaleString()}</p>
+                    `;
+                })
+                .catch(error => {
+                    console.error('获取系统状态失败:', error);
+                    document.getElementById('systemStatus').innerHTML = '<p class="status-error">获取系统状态失败</p>';
+                });
+        }
+
+        // 获取最近测试结果
+        function fetchRecentResults() {
+            fetch('/api/speed-test?limit=10')
+                .then(response => response.json())
+                .then(data => {
+                    const resultsDiv = document.getElementById('recentResults');
+                    if (data.data && data.data.length > 0) {
+                        let html = '<table class="table">';
+                        html += '<thead><tr><th>节点名称</th><th>延迟(ms)</th><th>IP地址</th><th>位置</th><th>状态</th><th>测试时间</th></tr></thead>';
+                        html += '<tbody>';
+                        
+                        data.data.forEach(result => {
+                            const status = result.is_success ? 
+                                '<span class="status-success">成功</span>' : 
+                                '<span class="status-error">失败</span>';
+                            
+                            html += `<tr>
+                                <td>${result.node ? result.node.name : '未知'}</td>
+                                <td>${result.latency || '-'}</td>
+                                <td>${result.ip_address || '-'}</td>
+                                <td>${result.location || '-'}</td>
+                                <td>${status}</td>
+                                <td>${new Date(result.test_time).toLocaleString()}</td>
+                            </tr>`;
+                        });
+                        
+                        html += '</tbody></table>';
+                        resultsDiv.innerHTML = html;
+                    } else {
+                        resultsDiv.innerHTML = '<p>暂无测试结果</p>';
+                    }
+                })
+                .catch(error => {
+                    console.error('获取测试结果失败:', error);
+                    document.getElementById('recentResults').innerHTML = '<p class="status-error">获取测试结果失败</p>';
+                });
+        }
+
+        // 触发测速
+        function triggerSpeedTest() {
+            fetch('/api/speed-test/trigger', {
+                method: 'POST'
+            })
+            .then(response => response.json())
+            .then(data => {
+                alert('测速任务已触发');
+                setTimeout(refreshData, 2000); // 2秒后刷新数据
+            })
+            .catch(error => {
+                console.error('触发测速失败:', error);
+                alert('触发测速失败');
+            });
+        }
+    </script>
+</body>
+</html> 

+ 87 - 0
go-speed-test/test_speed.go

@@ -0,0 +1,87 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"time"
+
+	"clash-speed-test/internal/config"
+	"clash-speed-test/internal/core"
+	"clash-speed-test/internal/database"
+)
+
+func main() {
+	fmt.Println("=== Clash 测速工具测试 ===")
+
+	// 加载配置
+	cfg, err := config.Load()
+	if err != nil {
+		log.Fatalf("加载配置失败: %v", err)
+	}
+
+	// 初始化测速器
+	speedTester := core.NewSpeedTester(cfg)
+
+	// 创建测试节点
+	testNodes := []database.Node{
+		{
+			Name:   "测试HTTP代理",
+			Type:   "http",
+			Server: "127.0.0.1",
+			Port:   7890,
+		},
+		{
+			Name:   "测试SOCKS5代理",
+			Type:   "socks5",
+			Server: "127.0.0.1",
+			Port:   7891,
+		},
+	}
+
+	fmt.Printf("开始测试 %d 个节点...\n", len(testNodes))
+
+	// 测试节点
+	results := speedTester.TestNodes(testNodes)
+
+	// 显示结果
+	fmt.Println("\n=== 测试结果 ===")
+	for i, result := range results {
+		fmt.Printf("\n节点 %d: %s\n", i+1, testNodes[i].Name)
+		fmt.Printf("  类型: %s\n", testNodes[i].Type)
+		fmt.Printf("  地址: %s:%d\n", testNodes[i].Server, testNodes[i].Port)
+		
+		if result.IsSuccess {
+			fmt.Printf("  状态: ✅ 成功\n")
+			if result.Latency != nil {
+				fmt.Printf("  延迟: %d ms\n", *result.Latency)
+			}
+			if result.DownloadSpeed != nil {
+				fmt.Printf("  下载速度: %.2f Mbps\n", float64(*result.DownloadSpeed))
+			}
+			if result.UploadSpeed != nil {
+				fmt.Printf("  上传速度: %.2f Mbps\n", float64(*result.UploadSpeed))
+			}
+			if result.IPAddress != "" {
+				fmt.Printf("  IP地址: %s\n", result.IPAddress)
+			}
+			if result.Location != "" {
+				fmt.Printf("  位置: %s\n", result.Location)
+			}
+		} else {
+			fmt.Printf("  状态: ❌ 失败\n")
+			if result.ErrorMessage != "" {
+				fmt.Printf("  错误: %s\n", result.ErrorMessage)
+			}
+		}
+		
+		if result.TestDuration != nil {
+			fmt.Printf("  测试耗时: %d ms\n", *result.TestDuration)
+		}
+	}
+
+	fmt.Println("\n=== 测试完成 ===")
+	fmt.Println("提示: 如果测试失败,请确保:")
+	fmt.Println("1. Clash客户端正在运行")
+	fmt.Println("2. 本地代理端口 (7890/7891) 可访问")
+	fmt.Println("3. 网络连接正常")
+} 

+ 7749 - 0
package-lock.json

@@ -0,0 +1,7749 @@
+{
+  "name": "clash-speed-test",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "clash-speed-test",
+      "version": "1.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "axios": "^1.6.0",
+        "compression": "^1.7.4",
+        "cors": "^2.8.5",
+        "dotenv": "^16.3.1",
+        "express": "^4.18.2",
+        "helmet": "^7.1.0",
+        "http-proxy-agent": "^7.0.2",
+        "https-proxy-agent": "^7.0.6",
+        "mysql2": "^3.6.5",
+        "node-cron": "^3.0.3",
+        "node-telegram-bot-api": "^0.64.0",
+        "proxy-agent": "^6.5.0",
+        "sequelize": "^6.35.0",
+        "shadowsocks-js": "^1.2.1",
+        "socks-proxy-agent": "^8.0.5",
+        "winston": "^3.11.0",
+        "yaml": "^2.3.4"
+      },
+      "devDependencies": {
+        "jest": "^29.7.0",
+        "nodemon": "^3.0.1",
+        "supertest": "^6.3.3"
+      }
+    },
+    "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "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
+    },
+    "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==",
+      "engines": {
+        "node": ">=0.1.90"
+      }
+    },
+    "node_modules/@cypress/request": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmmirror.com/@cypress/request/-/request-3.0.8.tgz",
+      "integrity": "sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==",
+      "dependencies": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~4.0.0",
+        "http-signature": "~1.4.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "performance-now": "^2.1.0",
+        "qs": "6.14.0",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "^5.0.0",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^8.3.2"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@cypress/request-promise": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/@cypress/request-promise/-/request-promise-5.0.0.tgz",
+      "integrity": "sha512-eKdYVpa9cBEw2kTBlHeu1PP16Blwtum6QHg/u9s/MoHkZfuo1pRGka1VlUHXF5kdew82BvOJVVGk0x8X0nbp+w==",
+      "dependencies": {
+        "bluebird": "^3.5.0",
+        "request-promise-core": "1.1.3",
+        "stealthy-require": "^1.1.1",
+        "tough-cookie": "^4.1.3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "peerDependencies": {
+        "@cypress/request": "^3.0.0"
+      }
+    },
+    "node_modules/@cypress/request-promise/node_modules/tough-cookie": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz",
+      "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+      "dependencies": {
+        "psl": "^1.1.33",
+        "punycode": "^2.1.1",
+        "universalify": "^0.2.0",
+        "url-parse": "^1.5.3"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@cypress/request/node_modules/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "dependencies": {
+        "colorspace": "1.1.x",
+        "enabled": "2.0.x",
+        "kuler": "^2.0.0"
+      }
+    },
+    "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,
+      "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/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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "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,
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "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,
+      "dependencies": {
+        "@noble/hashes": "^1.1.5"
+      }
+    },
+    "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
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "@sinonjs/commons": "^3.0.0"
+      }
+    },
+    "node_modules/@tootallnate/quickjs-emscripten": {
+      "version": "0.23.0",
+      "resolved": "https://registry.npmmirror.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+      "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="
+    },
+    "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,
+      "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,
+      "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,
+      "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,
+      "dependencies": {
+        "@babel/types": "^7.20.7"
+      }
+    },
+    "node_modules/@types/debug": {
+      "version": "4.1.12",
+      "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
+      "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+      "dependencies": {
+        "@types/ms": "*"
+      }
+    },
+    "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,
+      "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
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "@types/istanbul-lib-report": "*"
+      }
+    },
+    "node_modules/@types/ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz",
+      "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
+    },
+    "node_modules/@types/node": {
+      "version": "24.0.10",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.10.tgz",
+      "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
+      "dependencies": {
+        "undici-types": "~7.8.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
+    },
+    "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=="
+    },
+    "node_modules/@types/validator": {
+      "version": "13.15.2",
+      "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.2.tgz",
+      "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q=="
+    },
+    "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,
+      "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
+    },
+    "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==",
+      "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==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.3.tgz",
+      "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+      "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==",
+      "peer": true,
+      "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/align-text": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/align-text/-/align-text-0.1.4.tgz",
+      "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==",
+      "dependencies": {
+        "kind-of": "^3.0.2",
+        "longest": "^1.0.1",
+        "repeat-string": "^1.5.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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==",
+      "dev": true,
+      "dependencies": {
+        "type-fest": "^0.21.3"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "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==",
+      "dev": true,
+      "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==",
+      "dev": true,
+      "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,
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "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,
+      "dependencies": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "node_modules/array-buffer-byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+      "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "is-array-buffer": "^3.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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=="
+    },
+    "node_modules/array.prototype.findindex": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmmirror.com/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz",
+      "integrity": "sha512-LLm4mhxa9v8j0A/RPnpQAP4svXToJFh+Hp1pNYl5ZD5qpB4zdx/D4YjpVcETkhFbUKWO3iGMVLvrOnnmkAJT6A==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.6",
+        "es-object-atoms": "^1.0.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/arraybuffer.prototype.slice": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+      "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "is-array-buffer": "^3.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+      "dev": true
+    },
+    "node_modules/asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "dependencies": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "node_modules/assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/ast-types": {
+      "version": "0.13.4",
+      "resolved": "https://registry.npmmirror.com/ast-types/-/ast-types-0.13.4.tgz",
+      "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+      "dependencies": {
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/async": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz",
+      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
+    },
+    "node_modules/async-function": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz",
+      "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/available-typed-arrays": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+      "dependencies": {
+        "possible-typed-array-names": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/aws-ssl-profiles": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+      "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/aws4": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz",
+      "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="
+    },
+    "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==",
+      "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,
+      "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,
+      "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,
+      "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,
+      "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-polyfill": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmmirror.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
+      "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==",
+      "dependencies": {
+        "babel-runtime": "^6.26.0",
+        "core-js": "^2.5.0",
+        "regenerator-runtime": "^0.10.5"
+      }
+    },
+    "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,
+      "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,
+      "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/babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmmirror.com/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
+      "dependencies": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "node_modules/babel-runtime/node_modules/regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
+    },
+    "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
+    },
+    "node_modules/basic-ftp": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmmirror.com/basic-ftp/-/basic-ftp-5.0.5.tgz",
+      "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+      "dependencies": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/bl": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/bl/-/bl-1.2.3.tgz",
+      "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==",
+      "dependencies": {
+        "readable-stream": "^2.3.5",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "node_modules/bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+    },
+    "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==",
+      "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,
+      "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,
+      "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"
+        }
+      ],
+      "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,
+      "dependencies": {
+        "node-int64": "^0.4.0"
+      }
+    },
+    "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==",
+      "dev": true
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "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==",
+      "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,
+      "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,
+      "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"
+        }
+      ]
+    },
+    "node_modules/caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
+    },
+    "node_modules/center-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmmirror.com/center-align/-/center-align-0.1.3.tgz",
+      "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==",
+      "dependencies": {
+        "align-text": "^0.1.3",
+        "lazy-cache": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=0.10.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==",
+      "dev": true,
+      "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,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "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,
+      "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/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"
+        }
+      ],
+      "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
+    },
+    "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,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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,
+      "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
+    },
+    "node_modules/color": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz",
+      "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+      "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==",
+      "dev": true,
+      "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=="
+    },
+    "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==",
+      "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==",
+      "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=="
+    },
+    "node_modules/colors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/colors/-/colors-1.0.3.tgz",
+      "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
+      "engines": {
+        "node": ">=0.1.90"
+      }
+    },
+    "node_modules/colorspace": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/colorspace/-/colorspace-1.1.4.tgz",
+      "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+      "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==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "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,
+      "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==",
+      "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==",
+      "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
+    },
+    "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==",
+      "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==",
+      "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
+    },
+    "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==",
+      "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=="
+    },
+    "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
+    },
+    "node_modules/core-js": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-2.6.12.tgz",
+      "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
+      "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+      "hasInstallScript": true
+    },
+    "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=="
+    },
+    "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==",
+      "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,
+      "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,
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cycle": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/cycle/-/cycle-1.0.3.tgz",
+      "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
+      "dependencies": {
+        "assert-plus": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+      "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/data-view-buffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+      "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+      "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/inspect-js"
+      }
+    },
+    "node_modules/data-view-byte-offset": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+      "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "engines": {
+        "node": ">=0.10.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,
+      "peerDependencies": {
+        "babel-plugin-macros": "^3.1.0"
+      },
+      "peerDependenciesMeta": {
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/degenerator": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/degenerator/-/degenerator-5.0.1.tgz",
+      "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+      "dependencies": {
+        "ast-types": "^0.13.4",
+        "escodegen": "^2.1.0",
+        "esprima": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "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==",
+      "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==",
+      "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==",
+      "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,
+      "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,
+      "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,
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dottie": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/dottie/-/dottie-2.0.6.tgz",
+      "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
+    },
+    "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==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
+      "dependencies": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "node_modules/ecc-jsbn/node_modules/jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
+    },
+    "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=="
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.179",
+      "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
+      "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
+      "dev": true
+    },
+    "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,
+      "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==",
+      "dev": true
+    },
+    "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=="
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "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,
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/es-abstract": {
+      "version": "1.24.0",
+      "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.0.tgz",
+      "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.2",
+        "arraybuffer.prototype.slice": "^1.0.4",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "data-view-buffer": "^1.0.2",
+        "data-view-byte-length": "^1.0.2",
+        "data-view-byte-offset": "^1.0.1",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-set-tostringtag": "^2.1.0",
+        "es-to-primitive": "^1.3.0",
+        "function.prototype.name": "^1.1.8",
+        "get-intrinsic": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "get-symbol-description": "^1.1.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "internal-slot": "^1.1.0",
+        "is-array-buffer": "^3.0.5",
+        "is-callable": "^1.2.7",
+        "is-data-view": "^1.0.2",
+        "is-negative-zero": "^2.0.3",
+        "is-regex": "^1.2.1",
+        "is-set": "^2.0.3",
+        "is-shared-array-buffer": "^1.0.4",
+        "is-string": "^1.1.1",
+        "is-typed-array": "^1.1.15",
+        "is-weakref": "^1.1.1",
+        "math-intrinsics": "^1.1.0",
+        "object-inspect": "^1.13.4",
+        "object-keys": "^1.1.1",
+        "object.assign": "^4.1.7",
+        "own-keys": "^1.0.1",
+        "regexp.prototype.flags": "^1.5.4",
+        "safe-array-concat": "^1.1.3",
+        "safe-push-apply": "^1.0.0",
+        "safe-regex-test": "^1.1.0",
+        "set-proto": "^1.0.0",
+        "stop-iteration-iterator": "^1.1.0",
+        "string.prototype.trim": "^1.2.10",
+        "string.prototype.trimend": "^1.0.9",
+        "string.prototype.trimstart": "^1.0.8",
+        "typed-array-buffer": "^1.0.3",
+        "typed-array-byte-length": "^1.0.3",
+        "typed-array-byte-offset": "^1.0.4",
+        "typed-array-length": "^1.0.7",
+        "unbox-primitive": "^1.1.0",
+        "which-typed-array": "^1.1.19"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "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==",
+      "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==",
+      "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==",
+      "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/es-shim-unscopables": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+      "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-to-primitive": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+      "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+      "dependencies": {
+        "is-callable": "^1.2.7",
+        "is-date-object": "^1.0.5",
+        "is-symbol": "^1.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "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=="
+    },
+    "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,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/escodegen": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz",
+      "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+      "dependencies": {
+        "esprima": "^4.0.1",
+        "estraverse": "^5.2.0",
+        "esutils": "^2.0.2"
+      },
+      "bin": {
+        "escodegen": "bin/escodegen.js",
+        "esgenerate": "bin/esgenerate.js"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "optionalDependencies": {
+        "source-map": "~0.6.1"
+      }
+    },
+    "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==",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "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==",
+      "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==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-3.1.2.tgz",
+      "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
+    },
+    "node_modules/execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "dev": true,
+      "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,
+      "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==",
+      "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/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+    },
+    "node_modules/extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
+      "engines": [
+        "node >=0.6.0"
+      ]
+    },
+    "node_modules/eyes": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmmirror.com/eyes/-/eyes-0.1.8.tgz",
+      "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
+      "engines": {
+        "node": "> 0.1.90"
+      }
+    },
+    "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==",
+      "peer": true
+    },
+    "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=="
+    },
+    "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
+    },
+    "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,
+      "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=="
+    },
+    "node_modules/file-type": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmmirror.com/file-type/-/file-type-3.9.0.tgz",
+      "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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,
+      "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==",
+      "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": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "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=="
+    },
+    "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"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/for-each": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
+      "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+      "dependencies": {
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "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==",
+      "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,
+      "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==",
+      "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==",
+      "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
+    },
+    "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,
+      "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==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/function.prototype.name": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+      "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "functions-have-names": "^1.2.3",
+        "hasown": "^2.0.2",
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "dependencies": {
+        "is-property": "^1.0.2"
+      }
+    },
+    "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,
+      "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,
+      "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==",
+      "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,
+      "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==",
+      "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,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-symbol-description": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+      "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-uri": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmmirror.com/get-uri/-/get-uri-6.0.4.tgz",
+      "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
+      "dependencies": {
+        "basic-ftp": "^5.0.2",
+        "data-uri-to-buffer": "^6.0.2",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/get-uri/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/get-uri/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=="
+    },
+    "node_modules/getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
+      "dependencies": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "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,
+      "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": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/globalthis": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz",
+      "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "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
+    },
+    "node_modules/har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
+      "peer": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/har-validator": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmmirror.com/har-validator/-/har-validator-5.1.5.tgz",
+      "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
+      "deprecated": "this library is no longer supported",
+      "peer": true,
+      "dependencies": {
+        "ajv": "^6.12.3",
+        "har-schema": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/has-bigints": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz",
+      "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz",
+      "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+      "dependencies": {
+        "dunder-proto": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "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==",
+      "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==",
+      "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==",
+      "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
+    },
+    "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==",
+      "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/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/http-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==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/http-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=="
+    },
+    "node_modules/http-signature": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.4.0.tgz",
+      "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
+      "dependencies": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^2.0.2",
+        "sshpk": "^1.18.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "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==",
+      "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==",
+      "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=="
+    },
+    "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,
+      "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==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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
+    },
+    "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,
+      "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,
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflection": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz",
+      "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
+      "engines": [
+        "node >= 0.4.0"
+      ]
+    },
+    "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,
+      "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=="
+    },
+    "node_modules/internal-slot": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
+      "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "hasown": "^2.0.2",
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ip": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmmirror.com/ip/-/ip-1.1.9.tgz",
+      "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ=="
+    },
+    "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==",
+      "dependencies": {
+        "jsbn": "1.1.0",
+        "sprintf-js": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ip-address/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=="
+    },
+    "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==",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-array-buffer": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+      "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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
+    },
+    "node_modules/is-async-function": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz",
+      "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+      "dependencies": {
+        "async-function": "^1.0.0",
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-bigint": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz",
+      "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+      "dependencies": {
+        "has-bigints": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-boolean-object": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+      "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+    },
+    "node_modules/is-callable": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-data-view": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz",
+      "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "is-typed-array": "^1.1.13"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-date-object": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz",
+      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.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,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-finalizationregistry": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+      "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "dev": true,
+      "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,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/is-generator-function": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz",
+      "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.0",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-map": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
+      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-negative-zero": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-number-object": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz",
+      "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
+    },
+    "node_modules/is-regex": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-set": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz",
+      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-shared-array-buffer": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+      "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-string": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz",
+      "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-symbol": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz",
+      "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-typed-array": {
+      "version": "1.1.15",
+      "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz",
+      "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+      "dependencies": {
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
+    },
+    "node_modules/is-weakmap": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz",
+      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakref": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz",
+      "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakset": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz",
+      "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true
+    },
+    "node_modules/isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
+    },
+    "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "dependencies": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "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=="
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "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
+    },
+    "node_modules/json-schema": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz",
+      "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
+    },
+    "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==",
+      "peer": true
+    },
+    "node_modules/json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
+    },
+    "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,
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsprim": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-2.0.2.tgz",
+      "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
+      "engines": [
+        "node >=0.6.0"
+      ],
+      "dependencies": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.4.0",
+        "verror": "1.10.0"
+      }
+    },
+    "node_modules/kind-of": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-3.2.2.tgz",
+      "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+      "dependencies": {
+        "is-buffer": "^1.1.5"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/kuler": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz",
+      "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
+    },
+    "node_modules/lazy-cache": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/lazy-cache/-/lazy-cache-1.0.4.tgz",
+      "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "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
+    },
+    "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,
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "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==",
+      "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=="
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
+    },
+    "node_modules/longest": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/longest/-/longest-1.0.1.tgz",
+      "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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,
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/lru.min": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz",
+      "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=1.30.0",
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wellwelwel"
+      }
+    },
+    "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,
+      "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,
+      "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,
+      "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==",
+      "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==",
+      "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==",
+      "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
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "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,
+      "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==",
+      "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==",
+      "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==",
+      "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==",
+      "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==",
+      "dev": true,
+      "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,
+      "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==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/moment-timezone": {
+      "version": "0.5.48",
+      "resolved": "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz",
+      "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+      "dependencies": {
+        "moment": "^2.29.4"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+    },
+    "node_modules/mysql2": {
+      "version": "3.14.1",
+      "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.14.1.tgz",
+      "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==",
+      "dependencies": {
+        "aws-ssl-profiles": "^1.1.1",
+        "denque": "^2.1.0",
+        "generate-function": "^2.3.1",
+        "iconv-lite": "^0.6.3",
+        "long": "^5.2.1",
+        "lru.min": "^1.0.0",
+        "named-placeholders": "^1.1.3",
+        "seq-queue": "^0.0.5",
+        "sqlstring": "^2.3.2"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      }
+    },
+    "node_modules/mysql2/node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/named-placeholders": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz",
+      "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+      "dependencies": {
+        "lru-cache": "^7.14.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/named-placeholders/node_modules/lru-cache": {
+      "version": "7.18.3",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz",
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.4",
+      "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz",
+      "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/netmask": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/netmask/-/netmask-2.0.2.tgz",
+      "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "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==",
+      "dependencies": {
+        "uuid": "8.3.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "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
+    },
+    "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
+    },
+    "node_modules/node-telegram-bot-api": {
+      "version": "0.64.0",
+      "resolved": "https://registry.npmmirror.com/node-telegram-bot-api/-/node-telegram-bot-api-0.64.0.tgz",
+      "integrity": "sha512-/gxCuaEDUyWMBiHInP0ufopUkaaKprXiv3lyP9MMZdPy2KPfYKNYNKfd1Ph7o9KhfURDtOYowPZCi4UCr+2caw==",
+      "dependencies": {
+        "@cypress/request": "^3.0.1",
+        "@cypress/request-promise": "^5.0.0",
+        "array.prototype.findindex": "^2.0.2",
+        "bl": "^1.2.3",
+        "debug": "^3.2.7",
+        "eventemitter3": "^3.0.0",
+        "file-type": "^3.9.0",
+        "mime": "^1.6.0",
+        "pump": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
+    "node_modules/node-telegram-bot-api/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/node-telegram-bot-api/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=="
+    },
+    "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,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "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,
+      "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,
+      "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,
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "peer": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "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==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.assign": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz",
+      "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "object-keys": "^1.1.1"
+      },
+      "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==",
+      "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==",
+      "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==",
+      "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==",
+      "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==",
+      "dev": true,
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/own-keys": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz",
+      "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.6",
+        "object-keys": "^1.1.1",
+        "safe-push-apply": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "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,
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-locate/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,
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "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,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pac-proxy-agent": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+      "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
+      "dependencies": {
+        "@tootallnate/quickjs-emscripten": "^0.23.0",
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "get-uri": "^6.0.1",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.6",
+        "pac-resolver": "^7.0.1",
+        "socks-proxy-agent": "^8.0.5"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/pac-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==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pac-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=="
+    },
+    "node_modules/pac-resolver": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/pac-resolver/-/pac-resolver-7.0.1.tgz",
+      "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+      "dependencies": {
+        "degenerator": "^5.0.0",
+        "netmask": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "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,
+      "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==",
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "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=="
+    },
+    "node_modules/performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.9.1",
+      "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
+      "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="
+    },
+    "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
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "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,
+      "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,
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/possible-typed-array-names": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+      "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "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,
+      "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,
+      "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=="
+    },
+    "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,
+      "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==",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/proxy-agent": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.5.0.tgz",
+      "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "http-proxy-agent": "^7.0.1",
+        "https-proxy-agent": "^7.0.6",
+        "lru-cache": "^7.14.1",
+        "pac-proxy-agent": "^7.1.0",
+        "proxy-from-env": "^1.1.0",
+        "socks-proxy-agent": "^8.0.5"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/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==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/proxy-agent/node_modules/lru-cache": {
+      "version": "7.18.3",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz",
+      "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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=="
+    },
+    "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=="
+    },
+    "node_modules/pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
+    },
+    "node_modules/psl": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz",
+      "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/lupomontero"
+      }
+    },
+    "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
+    },
+    "node_modules/pump": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/pump/-/pump-2.0.1.tgz",
+      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "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"
+        }
+      ]
+    },
+    "node_modules/qs": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+      "dependencies": {
+        "side-channel": "^1.0.6"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/querystringify": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz",
+      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+    },
+    "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==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "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==",
+      "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
+    },
+    "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==",
+      "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=="
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/reflect.getprototypeof": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+      "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.1",
+        "which-builtin-type": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/regenerator-runtime": {
+      "version": "0.10.5",
+      "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+      "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w=="
+    },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmmirror.com/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/request": {
+      "version": "2.88.2",
+      "resolved": "https://registry.npmmirror.com/request/-/request-2.88.2.tgz",
+      "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+      "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
+      "peer": true,
+      "dependencies": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.3",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.5.0",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/request-promise-core": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/request-promise-core/-/request-promise-core-1.1.3.tgz",
+      "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
+      "dependencies": {
+        "lodash": "^4.17.15"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "peerDependencies": {
+        "request": "^2.34"
+      }
+    },
+    "node_modules/request/node_modules/form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "peer": true,
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 0.12"
+      }
+    },
+    "node_modules/request/node_modules/http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
+      "peer": true,
+      "dependencies": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      },
+      "engines": {
+        "node": ">=0.8",
+        "npm": ">=1.3.7"
+      }
+    },
+    "node_modules/request/node_modules/jsprim": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz",
+      "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
+      "peer": true,
+      "dependencies": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.4.0",
+        "verror": "1.10.0"
+      },
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
+    "node_modules/request/node_modules/qs": {
+      "version": "6.5.3",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.5.3.tgz",
+      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
+      "peer": true,
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/request/node_modules/tough-cookie": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-2.5.0.tgz",
+      "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+      "peer": true,
+      "dependencies": {
+        "psl": "^1.1.28",
+        "punycode": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/request/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "peer": true,
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/retry-as-promised": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
+      "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw=="
+    },
+    "node_modules/right-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmmirror.com/right-align/-/right-align-0.1.3.tgz",
+      "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==",
+      "dependencies": {
+        "align-text": "^0.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/safe-array-concat": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+      "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "has-symbols": "^1.1.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">=0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-array-concat/node_modules/isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+    },
+    "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"
+        }
+      ]
+    },
+    "node_modules/safe-push-apply": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+      "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-push-apply/node_modules/isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+    },
+    "node_modules/safe-regex-test": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "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=="
+    },
+    "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,
+      "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==",
+      "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==",
+      "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=="
+    },
+    "node_modules/seq-queue": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz",
+      "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+    },
+    "node_modules/sequelize": {
+      "version": "6.37.7",
+      "resolved": "https://registry.npmmirror.com/sequelize/-/sequelize-6.37.7.tgz",
+      "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/sequelize"
+        }
+      ],
+      "dependencies": {
+        "@types/debug": "^4.1.8",
+        "@types/validator": "^13.7.17",
+        "debug": "^4.3.4",
+        "dottie": "^2.0.6",
+        "inflection": "^1.13.4",
+        "lodash": "^4.17.21",
+        "moment": "^2.29.4",
+        "moment-timezone": "^0.5.43",
+        "pg-connection-string": "^2.6.1",
+        "retry-as-promised": "^7.0.4",
+        "semver": "^7.5.4",
+        "sequelize-pool": "^7.1.0",
+        "toposort-class": "^1.0.1",
+        "uuid": "^8.3.2",
+        "validator": "^13.9.0",
+        "wkx": "^0.5.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ibm_db": {
+          "optional": true
+        },
+        "mariadb": {
+          "optional": true
+        },
+        "mysql2": {
+          "optional": true
+        },
+        "oracledb": {
+          "optional": true
+        },
+        "pg": {
+          "optional": true
+        },
+        "pg-hstore": {
+          "optional": true
+        },
+        "snowflake-sdk": {
+          "optional": true
+        },
+        "sqlite3": {
+          "optional": true
+        },
+        "tedious": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sequelize-pool": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmmirror.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
+      "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/sequelize/node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sequelize/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=="
+    },
+    "node_modules/sequelize/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==",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "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==",
+      "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/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-function-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz",
+      "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "node_modules/shadowsocks-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/shadowsocks-js/-/shadowsocks-js-1.2.1.tgz",
+      "integrity": "sha512-aWu3uwnuhRolYNeZBkqF23xg9ddximTOVSF7Bg5h1wTS4+b8A/rrdD+ziKiJl18H7fsjjjZCR6y840JIZCOuxw==",
+      "dependencies": {
+        "babel-polyfill": "^6.16.0",
+        "ip": "^1.1.3",
+        "lru-cache": "^4.0.1",
+        "minimist": "^1.2.0",
+        "uglify-js": "^2.6.2",
+        "winston": "^2.2.0"
+      },
+      "bin": {
+        "localssjs": "bin/localssjs",
+        "serverssjs": "bin/serverssjs"
+      }
+    },
+    "node_modules/shadowsocks-js/node_modules/async": {
+      "version": "2.6.4",
+      "resolved": "https://registry.npmmirror.com/async/-/async-2.6.4.tgz",
+      "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+      "dependencies": {
+        "lodash": "^4.17.14"
+      }
+    },
+    "node_modules/shadowsocks-js/node_modules/lru-cache": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-4.1.5.tgz",
+      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "dependencies": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "node_modules/shadowsocks-js/node_modules/winston": {
+      "version": "2.4.7",
+      "resolved": "https://registry.npmmirror.com/winston/-/winston-2.4.7.tgz",
+      "integrity": "sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==",
+      "dependencies": {
+        "async": "^2.6.4",
+        "colors": "1.0.x",
+        "cycle": "1.0.x",
+        "eyes": "0.1.x",
+        "isstream": "0.1.x",
+        "stack-trace": "0.0.x"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/shadowsocks-js/node_modules/yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+    },
+    "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,
+      "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,
+      "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==",
+      "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==",
+      "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==",
+      "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==",
+      "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==",
+      "dev": true
+    },
+    "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==",
+      "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=="
+    },
+    "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,
+      "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,
+      "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
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "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==",
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.5.tgz",
+      "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
+      "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==",
+      "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==",
+      "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=="
+    },
+    "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==",
+      "devOptional": true,
+      "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,
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "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
+    },
+    "node_modules/sqlstring": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz",
+      "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/sshpk": {
+      "version": "1.18.0",
+      "resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz",
+      "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
+      "dependencies": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      },
+      "bin": {
+        "sshpk-conv": "bin/sshpk-conv",
+        "sshpk-sign": "bin/sshpk-sign",
+        "sshpk-verify": "bin/sshpk-verify"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sshpk/node_modules/jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
+    },
+    "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==",
+      "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,
+      "dependencies": {
+        "escape-string-regexp": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/stealthy-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/stealthy-require/-/stealthy-require-1.1.1.tgz",
+      "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stop-iteration-iterator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+      "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "internal-slot": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "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==",
+      "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=="
+    },
+    "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,
+      "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==",
+      "dev": true,
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string.prototype.trim": {
+      "version": "1.2.10",
+      "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+      "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-data-property": "^1.1.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-object-atoms": "^1.0.0",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimend": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+      "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimstart": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+      "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "dev": true,
+      "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,
+      "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,
+      "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,
+      "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 v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net",
+      "dev": true,
+      "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,
+      "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,
+      "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
+    },
+    "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,
+      "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==",
+      "dev": true,
+      "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==",
+      "dev": true,
+      "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,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "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=="
+    },
+    "node_modules/tldts": {
+      "version": "6.1.86",
+      "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz",
+      "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+      "dependencies": {
+        "tldts-core": "^6.1.86"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "6.1.86",
+      "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz",
+      "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="
+    },
+    "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
+    },
+    "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,
+      "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==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/toposort-class": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/toposort-class/-/toposort-class-1.0.1.tgz",
+      "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg=="
+    },
+    "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,
+      "bin": {
+        "nodetouch": "bin/nodetouch.js"
+      }
+    },
+    "node_modules/tough-cookie": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz",
+      "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+      "dependencies": {
+        "tldts": "^6.1.32"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "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==",
+      "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=="
+    },
+    "node_modules/tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
+    },
+    "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,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "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==",
+      "dev": true,
+      "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==",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typed-array-buffer": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/typed-array-byte-length": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+      "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-byte-offset": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+      "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.15",
+        "reflect.getprototypeof": "^1.0.9"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-length": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz",
+      "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0",
+        "reflect.getprototypeof": "^1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/uglify-js": {
+      "version": "2.8.29",
+      "resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-2.8.29.tgz",
+      "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==",
+      "dependencies": {
+        "source-map": "~0.5.1",
+        "yargs": "~3.10.0"
+      },
+      "bin": {
+        "uglifyjs": "bin/uglifyjs"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      },
+      "optionalDependencies": {
+        "uglify-to-browserify": "~1.0.0"
+      }
+    },
+    "node_modules/uglify-js/node_modules/camelcase": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-1.2.1.tgz",
+      "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/uglify-js/node_modules/cliui": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/cliui/-/cliui-2.1.0.tgz",
+      "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==",
+      "dependencies": {
+        "center-align": "^0.1.1",
+        "right-align": "^0.1.1",
+        "wordwrap": "0.0.2"
+      }
+    },
+    "node_modules/uglify-js/node_modules/source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/uglify-js/node_modules/yargs": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmmirror.com/yargs/-/yargs-3.10.0.tgz",
+      "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==",
+      "dependencies": {
+        "camelcase": "^1.0.2",
+        "cliui": "^2.1.0",
+        "decamelize": "^1.0.0",
+        "window-size": "0.1.0"
+      }
+    },
+    "node_modules/uglify-to-browserify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+      "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==",
+      "optional": true
+    },
+    "node_modules/unbox-primitive": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+      "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-bigints": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "which-boxed-primitive": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+      "dev": true
+    },
+    "node_modules/undici-types": {
+      "version": "7.8.0",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.8.0.tgz",
+      "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
+    },
+    "node_modules/universalify": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz",
+      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "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"
+        }
+      ],
+      "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==",
+      "peer": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/url-parse": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz",
+      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+      "dependencies": {
+        "querystringify": "^2.1.1",
+        "requires-port": "^1.0.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=="
+    },
+    "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==",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "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==",
+      "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,
+      "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.15.15",
+      "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.15.tgz",
+      "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
+      "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==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
+      "engines": [
+        "node >=0.6.0"
+      ],
+      "dependencies": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "node_modules/verror/node_modules/core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
+    },
+    "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,
+      "dependencies": {
+        "makeerror": "1.0.12"
+      }
+    },
+    "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,
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which-boxed-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+      "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+      "dependencies": {
+        "is-bigint": "^1.1.0",
+        "is-boolean-object": "^1.2.1",
+        "is-number-object": "^1.1.1",
+        "is-string": "^1.1.1",
+        "is-symbol": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-builtin-type": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+      "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "function.prototype.name": "^1.1.6",
+        "has-tostringtag": "^1.0.2",
+        "is-async-function": "^2.0.0",
+        "is-date-object": "^1.1.0",
+        "is-finalizationregistry": "^1.1.0",
+        "is-generator-function": "^1.0.10",
+        "is-regex": "^1.2.1",
+        "is-weakref": "^1.0.2",
+        "isarray": "^2.0.5",
+        "which-boxed-primitive": "^1.1.0",
+        "which-collection": "^1.0.2",
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-builtin-type/node_modules/isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+    },
+    "node_modules/which-collection": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz",
+      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+      "dependencies": {
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-typed-array": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",
+      "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "for-each": "^0.3.5",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/window-size": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/window-size/-/window-size-0.1.0.tgz",
+      "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "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==",
+      "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-transport": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmmirror.com/winston-transport/-/winston-transport-4.9.0.tgz",
+      "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
+      "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==",
+      "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==",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/wkx": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmmirror.com/wkx/-/wkx-0.5.0.tgz",
+      "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/wordwrap": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-0.0.2.tgz",
+      "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "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,
+      "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/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+    },
+    "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,
+      "dependencies": {
+        "imurmurhash": "^0.1.4",
+        "signal-exit": "^3.0.7"
+      },
+      "engines": {
+        "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+      }
+    },
+    "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,
+      "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
+    },
+    "node_modules/yaml": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.0.tgz",
+      "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      }
+    },
+    "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,
+      "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,
+      "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,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}

+ 49 - 0
package.json

@@ -0,0 +1,49 @@
+{
+  "name": "clash-speed-test",
+  "version": "1.0.0",
+  "description": "Clash节点测速工具,支持定时监控和Telegram通知",
+  "main": "src/app.js",
+  "scripts": {
+    "start": "node src/app.js",
+    "dev": "nodemon src/app.js",
+    "test": "jest",
+    "migrate": "node src/database/migrate.js",
+    "seed": "node src/database/seed.js",
+    "launch": "node start.js",
+    "test-subscription": "node test-subscription.js",
+    "test-telegram": "node test-telegram.js",
+    "get-chat-id": "node get-chat-id.js"
+  },
+  "keywords": [
+    "clash",
+    "speed-test",
+    "telegram",
+    "monitoring"
+  ],
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "axios": "^1.6.0",
+    "compression": "^1.7.4",
+    "cors": "^2.8.5",
+    "dotenv": "^16.3.1",
+    "express": "^4.18.2",
+    "helmet": "^7.1.0",
+    "http-proxy-agent": "^7.0.2",
+    "https-proxy-agent": "^7.0.6",
+    "mysql2": "^3.6.5",
+    "node-cron": "^3.0.3",
+    "node-telegram-bot-api": "^0.64.0",
+    "proxy-agent": "^6.5.0",
+    "sequelize": "^6.35.0",
+    "shadowsocks-js": "^1.2.1",
+    "socks-proxy-agent": "^8.0.5",
+    "winston": "^3.11.0",
+    "yaml": "^2.3.4"
+  },
+  "devDependencies": {
+    "jest": "^29.7.0",
+    "nodemon": "^3.0.1",
+    "supertest": "^6.3.3"
+  }
+}

+ 865 - 0
public/app.js

@@ -0,0 +1,865 @@
+// 全局变量
+let currentPage = 'dashboard';
+let currentNodesPage = 1;
+let currentResultsPage = 1;
+let currentNotificationsPage = 1;
+
+// API基础URL
+const API_BASE = '/api';
+
+// 页面加载完成后初始化
+document.addEventListener('DOMContentLoaded', function() {
+    initializeApp();
+});
+
+// 初始化应用
+async function initializeApp() {
+    try {
+        // 设置导航事件
+        setupNavigation();
+        
+        // 加载仪表板数据
+        await loadDashboard();
+        
+        // 设置定时刷新
+        setInterval(loadDashboard, 30000); // 每30秒刷新一次
+        
+        console.log('应用初始化完成');
+    } catch (error) {
+        console.error('应用初始化失败:', error);
+        showError('应用初始化失败: ' + error.message);
+    }
+}
+
+// 设置导航
+function setupNavigation() {
+    const navLinks = document.querySelectorAll('[data-page]');
+    navLinks.forEach(link => {
+        link.addEventListener('click', function(e) {
+            e.preventDefault();
+            const page = this.getAttribute('data-page');
+            showPage(page);
+        });
+    });
+}
+
+// 显示页面
+async function showPage(pageName) {
+    // 隐藏所有页面
+    document.querySelectorAll('.page-content').forEach(page => {
+        page.style.display = 'none';
+    });
+    
+    // 移除所有导航激活状态
+    document.querySelectorAll('.nav-link').forEach(link => {
+        link.classList.remove('active');
+    });
+    
+    // 激活当前导航
+    document.querySelector(`[data-page="${pageName}"]`).classList.add('active');
+    
+    // 显示目标页面
+    const targetPage = document.getElementById(pageName + '-page');
+    if (targetPage) {
+        targetPage.style.display = 'block';
+        targetPage.classList.add('fade-in');
+    }
+    
+    // 加载页面数据
+    switch (pageName) {
+        case 'dashboard':
+            await loadDashboard();
+            break;
+        case 'nodes':
+            await loadNodes();
+            break;
+        case 'speed-test':
+            await loadTestResults();
+            break;
+        case 'notifications':
+            await loadNotifications();
+            break;
+        case 'settings':
+            await loadSettings();
+            break;
+    }
+    
+    currentPage = pageName;
+}
+
+// 加载仪表板
+async function loadDashboard() {
+    try {
+        showLoading();
+        
+        // 加载统计信息
+        const statsResponse = await fetch(`${API_BASE}/stats`);
+        const statsData = await statsResponse.json();
+        
+        if (statsData.success) {
+            updateDashboardStats(statsData.data);
+        }
+        
+        // 加载最近测速结果
+        const resultsResponse = await fetch(`${API_BASE}/test/results?limit=5`);
+        const resultsData = await resultsResponse.json();
+        
+        if (resultsData.success) {
+            updateRecentResults(resultsData.data.results);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('加载仪表板失败:', error);
+        hideLoading();
+        showError('加载仪表板失败: ' + error.message);
+    }
+}
+
+// 更新仪表板统计
+function updateDashboardStats(stats) {
+    document.getElementById('total-nodes').textContent = stats.nodes.total;
+    document.getElementById('online-nodes').textContent = stats.nodes.online;
+    document.getElementById('success-rate').textContent = stats.tests.successRate + '%';
+    document.getElementById('today-tests').textContent = stats.tests.recent24h;
+}
+
+// 更新最近测速结果
+function updateRecentResults(results) {
+    const container = document.getElementById('recent-results');
+    
+    if (results.length === 0) {
+        container.innerHTML = '<div class="text-center text-muted">暂无测速结果</div>';
+        return;
+    }
+    
+    const html = results.map(result => `
+        <div class="result-item ${result.isSuccess ? 'result-success' : 'result-failure'}">
+            <div class="result-header">
+                <div class="result-node">${result.node ? result.node.name : '未知节点'}</div>
+                <div class="result-time">${formatTime(result.testTime)}</div>
+            </div>
+            <div class="result-details">
+                <div class="result-detail">
+                    <i class="bi ${result.isSuccess ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
+                    状态: ${result.isSuccess ? '成功' : '失败'}
+                </div>
+                ${result.isSuccess ? `
+                    <div class="result-detail">
+                        <i class="bi bi-speedometer2"></i>
+                        延迟: ${result.latency}ms
+                    </div>
+                    <div class="result-detail">
+                        <i class="bi bi-download"></i>
+                        下载: ${formatSpeed(result.downloadSpeed)}
+                    </div>
+                    <div class="result-detail">
+                        <i class="bi bi-upload"></i>
+                        上传: ${formatSpeed(result.uploadSpeed)}
+                    </div>
+                ` : `
+                    <div class="result-detail">
+                        <i class="bi bi-exclamation-triangle"></i>
+                        错误: ${result.error || '未知错误'}
+                    </div>
+                `}
+            </div>
+        </div>
+    `).join('');
+    
+    container.innerHTML = html;
+}
+
+// 加载节点列表
+async function loadNodes(page = 1) {
+    try {
+        showLoading();
+        
+        const search = document.getElementById('node-search')?.value || '';
+        const group = document.getElementById('group-filter')?.value || '';
+        const status = document.getElementById('status-filter')?.value || '';
+        
+        const params = new URLSearchParams({
+            page: page,
+            limit: 20,
+            ...(search && { search }),
+            ...(group && { group }),
+            ...(status && { status })
+        });
+        
+        const response = await fetch(`${API_BASE}/nodes?${params}`);
+        const data = await response.json();
+        
+        if (data.success) {
+            updateNodesList(data.data);
+            currentNodesPage = page;
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('加载节点列表失败:', error);
+        hideLoading();
+        showError('加载节点列表失败: ' + error.message);
+    }
+}
+
+// 更新节点列表
+function updateNodesList(data) {
+    const container = document.getElementById('nodes-list');
+    const { nodes, pagination } = data;
+    
+    if (nodes.length === 0) {
+        container.innerHTML = '<div class="text-center text-muted">暂无节点</div>';
+        return;
+    }
+    
+    const html = nodes.map(node => `
+        <div class="node-item">
+            <div class="node-header">
+                <div class="node-name">${node.name}</div>
+                <div class="node-status ${node.status}">${node.status === 'online' ? '在线' : '离线'}</div>
+            </div>
+            <div class="node-info">
+                <div class="node-info-item">
+                    <i class="bi bi-hdd-network"></i>
+                    类型: ${node.type}
+                </div>
+                <div class="node-info-item">
+                    <i class="bi bi-geo-alt"></i>
+                    服务器: ${node.server}
+                </div>
+                <div class="node-info-item">
+                    <i class="bi bi-arrow-left-right"></i>
+                    端口: ${node.port}
+                </div>
+                <div class="node-info-item">
+                    <i class="bi bi-tags"></i>
+                    分组: ${node.group || '默认'}
+                </div>
+                ${node.testResults && node.testResults.length > 0 ? `
+                    <div class="node-info-item">
+                        <i class="bi bi-clock"></i>
+                        最后测速: ${formatTime(node.testResults[0].testTime)}
+                    </div>
+                ` : ''}
+            </div>
+            <div class="mt-2">
+                <button class="btn btn-sm btn-outline-primary" onclick="viewNodeDetail(${node.id})">
+                    <i class="bi bi-eye"></i> 详情
+                </button>
+                <button class="btn btn-sm btn-outline-success" onclick="testSingleNode(${node.id})">
+                    <i class="bi bi-speedometer"></i> 测速
+                </button>
+            </div>
+        </div>
+    `).join('');
+    
+    container.innerHTML = html;
+    
+    // 更新分页
+    updatePagination('nodes-pagination', pagination, loadNodes);
+}
+
+// 加载测速结果
+async function loadTestResults(page = 1) {
+    try {
+        showLoading();
+        
+        const nodeId = document.getElementById('result-node-filter')?.value || '';
+        const isSuccess = document.getElementById('result-status-filter')?.value || '';
+        const startDate = document.getElementById('start-date')?.value || '';
+        const endDate = document.getElementById('end-date')?.value || '';
+        
+        const params = new URLSearchParams({
+            page: page,
+            limit: 20,
+            ...(nodeId && { nodeId }),
+            ...(isSuccess && { isSuccess }),
+            ...(startDate && { startDate }),
+            ...(endDate && { endDate })
+        });
+        
+        const response = await fetch(`${API_BASE}/test/results?${params}`);
+        const data = await response.json();
+        
+        if (data.success) {
+            updateTestResults(data.data);
+            currentResultsPage = page;
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('加载测速结果失败:', error);
+        hideLoading();
+        showError('加载测速结果失败: ' + error.message);
+    }
+}
+
+// 更新测速结果
+function updateTestResults(data) {
+    const container = document.getElementById('test-results');
+    const { results, pagination } = data;
+    
+    if (results.length === 0) {
+        container.innerHTML = '<div class="text-center text-muted">暂无测速结果</div>';
+        return;
+    }
+    
+    const html = results.map(result => `
+        <div class="result-item ${result.isSuccess ? 'result-success' : 'result-failure'}">
+            <div class="result-header">
+                <div class="result-node">${result.node ? result.node.name : '未知节点'}</div>
+                <div class="result-time">${formatTime(result.testTime)}</div>
+            </div>
+            <div class="result-details">
+                <div class="result-detail">
+                    <i class="bi ${result.isSuccess ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
+                    状态: ${result.isSuccess ? '成功' : '失败'}
+                </div>
+                ${result.isSuccess ? `
+                    <div class="result-detail">
+                        <i class="bi bi-speedometer2"></i>
+                        延迟: ${result.latency}ms
+                    </div>
+                    <div class="result-detail">
+                        <i class="bi bi-download"></i>
+                        下载: ${formatSpeed(result.downloadSpeed)}
+                    </div>
+                    <div class="result-detail">
+                        <i class="bi bi-upload"></i>
+                        上传: ${formatSpeed(result.uploadSpeed)}
+                    </div>
+                ` : `
+                    <div class="result-detail">
+                        <i class="bi bi-exclamation-triangle"></i>
+                        错误: ${result.error || '未知错误'}
+                    </div>
+                `}
+            </div>
+        </div>
+    `).join('');
+    
+    container.innerHTML = html;
+    
+    // 更新分页
+    updatePagination('results-pagination', pagination, loadTestResults);
+}
+
+// 加载通知记录
+async function loadNotifications(page = 1) {
+    try {
+        showLoading();
+        
+        const params = new URLSearchParams({
+            page: page,
+            limit: 20
+        });
+        
+        const response = await fetch(`${API_BASE}/notifications?${params}`);
+        const data = await response.json();
+        
+        if (data.success) {
+            updateNotificationsList(data.data);
+            currentNotificationsPage = page;
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('加载通知记录失败:', error);
+        hideLoading();
+        showError('加载通知记录失败: ' + error.message);
+    }
+}
+
+// 更新通知列表
+function updateNotificationsList(data) {
+    const container = document.getElementById('notifications-list');
+    const { notifications, pagination } = data;
+    
+    if (notifications.length === 0) {
+        container.innerHTML = '<div class="text-center text-muted">暂无通知记录</div>';
+        return;
+    }
+    
+    const html = notifications.map(notification => `
+        <div class="notification-item ${notification.isSent ? 'notification-sent' : 'notification-failed'}">
+            <div class="result-header">
+                <div class="result-node">${notification.type}</div>
+                <div class="result-time">${formatTime(notification.createdAt)}</div>
+            </div>
+            <div class="result-details">
+                <div class="result-detail">
+                    <i class="bi ${notification.isSent ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
+                    状态: ${notification.isSent ? '已发送' : '发送失败'}
+                </div>
+                <div class="result-detail">
+                    <i class="bi bi-chat"></i>
+                    内容: ${notification.message.substring(0, 100)}${notification.message.length > 100 ? '...' : ''}
+                </div>
+                ${notification.node ? `
+                    <div class="result-detail">
+                        <i class="bi bi-hdd-network"></i>
+                        节点: ${notification.node.name}
+                    </div>
+                ` : ''}
+            </div>
+        </div>
+    `).join('');
+    
+    container.innerHTML = html;
+    
+    // 更新分页
+    updatePagination('notifications-pagination', pagination, loadNotifications);
+}
+
+// 加载设置页面
+async function loadSettings() {
+    try {
+        showLoading();
+        
+        // 加载系统状态
+        const statusResponse = await fetch(`${API_BASE}/status`);
+        const statusData = await statusResponse.json();
+        
+        if (statusData.success) {
+            updateSystemStatus(statusData.data);
+        }
+        
+        // 加载订阅状态
+        const subscriptionResponse = await fetch(`${API_BASE}/subscription/status`);
+        const subscriptionData = await subscriptionResponse.json();
+        
+        if (subscriptionData.success) {
+            updateSubscriptionStatus(subscriptionData.data);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('加载设置失败:', error);
+        hideLoading();
+        showError('加载设置失败: ' + error.message);
+    }
+}
+
+// 更新系统状态
+function updateSystemStatus(status) {
+    const container = document.getElementById('system-status');
+    
+    const html = `
+        <div class="row">
+            <div class="col-md-6">
+                <h6>调度器状态</h6>
+                <div class="mb-2">
+                    <span class="badge ${status.isRunning ? 'bg-success' : 'bg-danger'}">
+                        ${status.isRunning ? '运行中' : '已停止'}
+                    </span>
+                </div>
+                <div class="mb-2">
+                    <small class="text-muted">下次测速: ${status.nextTestTime ? formatTime(status.nextTestTime) : '未设置'}</small>
+                </div>
+                <div class="mb-2">
+                    <small class="text-muted">测速间隔: ${status.testInterval ? Math.round(status.testInterval / 60000) + '分钟' : '未设置'}</small>
+                </div>
+            </div>
+            <div class="col-md-6">
+                <h6>系统信息</h6>
+                <div class="mb-2">
+                    <small class="text-muted">运行时间: ${formatUptime(status.uptime)}</small>
+                </div>
+                <div class="mb-2">
+                    <small class="text-muted">总测速次数: ${status.totalTests || 0}</small>
+                </div>
+            </div>
+        </div>
+    `;
+    
+    container.innerHTML = html;
+}
+
+// 更新订阅状态
+function updateSubscriptionStatus(status) {
+    const container = document.getElementById('subscription-status');
+    
+    const html = `
+        <div class="mb-2">
+            <strong>订阅地址:</strong> ${status.subscriptionUrl || '未配置'}
+        </div>
+        <div class="mb-2">
+            <strong>更新间隔:</strong> ${status.updateInterval ? Math.round(status.updateInterval / 60000) + '分钟' : '未设置'}
+        </div>
+        <div class="mb-2">
+            <strong>自动更新:</strong> 
+            <span class="badge ${status.autoUpdateEnabled ? 'bg-success' : 'bg-secondary'}">
+                ${status.autoUpdateEnabled ? '已启用' : '已禁用'}
+            </span>
+        </div>
+        ${status.lastUpdate ? `
+            <div class="mb-2">
+                <strong>最后更新:</strong> ${formatTime(status.lastUpdate)}
+            </div>
+        ` : ''}
+    `;
+    
+    container.innerHTML = html;
+}
+
+// 开始测速
+async function startSpeedTest() {
+    try {
+        showLoading();
+        
+        const response = await fetch(`${API_BASE}/test/manual`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({})
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            showSuccess(`测速已开始,共测试 ${data.data.testedNodes} 个节点`);
+            // 刷新当前页面数据
+            if (currentPage === 'dashboard') {
+                await loadDashboard();
+            } else if (currentPage === 'speed-test') {
+                await loadTestResults();
+            }
+        } else {
+            showError('测速失败: ' + data.error);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('测速失败:', error);
+        hideLoading();
+        showError('测速失败: ' + error.message);
+    }
+}
+
+// 测试通知
+async function testNotification() {
+    try {
+        showLoading();
+        
+        const response = await fetch(`${API_BASE}/notifications/test`, {
+            method: 'POST'
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            showSuccess('通知测试成功');
+        } else {
+            showError('通知测试失败: ' + data.error);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('通知测试失败:', error);
+        hideLoading();
+        showError('通知测试失败: ' + error.message);
+    }
+}
+
+// 刷新统计
+async function refreshStats() {
+    await loadDashboard();
+    showSuccess('统计信息已刷新');
+}
+
+// 显示系统状态
+async function showSystemStatus() {
+    await showPage('settings');
+}
+
+// 搜索节点
+function searchNodes() {
+    loadNodes(1);
+}
+
+// 搜索结果
+function searchResults() {
+    loadTestResults(1);
+}
+
+// 导入节点
+function importNodes() {
+    const modal = new bootstrap.Modal(document.getElementById('importModal'));
+    modal.show();
+}
+
+// 确认导入
+async function confirmImport() {
+    try {
+        const configPath = document.getElementById('config-path').value;
+        const configUrl = document.getElementById('config-url').value;
+        
+        if (!configPath && !configUrl) {
+            showError('请提供配置文件路径或URL');
+            return;
+        }
+        
+        showLoading();
+        
+        const response = await fetch(`${API_BASE}/import/clash`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                configPath: configPath || undefined,
+                configUrl: configUrl || undefined
+            })
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            showSuccess(`成功导入 ${data.data.imported} 个节点`);
+            bootstrap.Modal.getInstance(document.getElementById('importModal')).hide();
+            if (currentPage === 'nodes') {
+                await loadNodes();
+            }
+        } else {
+            showError('导入失败: ' + data.error);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('导入失败:', error);
+        hideLoading();
+        showError('导入失败: ' + error.message);
+    }
+}
+
+// 更新订阅
+async function updateSubscription() {
+    try {
+        showLoading();
+        
+        const response = await fetch(`${API_BASE}/subscription/update`, {
+            method: 'POST'
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            showSuccess('订阅更新成功');
+            await loadSettings();
+        } else {
+            showError('订阅更新失败: ' + data.error);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('订阅更新失败:', error);
+        hideLoading();
+        showError('订阅更新失败: ' + error.message);
+    }
+}
+
+// 查看节点详情
+async function viewNodeDetail(nodeId) {
+    try {
+        const response = await fetch(`${API_BASE}/nodes/${nodeId}`);
+        const data = await response.json();
+        
+        if (data.success) {
+            const node = data.data;
+            const modal = new bootstrap.Modal(document.getElementById('nodeDetailModal'));
+            
+            document.getElementById('node-detail-content').innerHTML = `
+                <div class="row">
+                    <div class="col-md-6">
+                        <h6>基本信息</h6>
+                        <table class="table table-sm">
+                            <tr><td>名称:</td><td>${node.name}</td></tr>
+                            <tr><td>类型:</td><td>${node.type}</td></tr>
+                            <tr><td>服务器:</td><td>${node.server}</td></tr>
+                            <tr><td>端口:</td><td>${node.port}</td></tr>
+                            <tr><td>分组:</td><td>${node.group || '默认'}</td></tr>
+                            <tr><td>状态:</td><td><span class="badge ${node.status === 'online' ? 'bg-success' : 'bg-danger'}">${node.status === 'online' ? '在线' : '离线'}</span></td></tr>
+                        </table>
+                    </div>
+                    <div class="col-md-6">
+                        <h6>最近测速结果</h6>
+                        ${node.testResults && node.testResults.length > 0 ? `
+                            <div class="result-item ${node.testResults[0].isSuccess ? 'result-success' : 'result-failure'}">
+                                <div class="result-header">
+                                    <div class="result-time">${formatTime(node.testResults[0].testTime)}</div>
+                                </div>
+                                <div class="result-details">
+                                    <div class="result-detail">
+                                        <i class="bi ${node.testResults[0].isSuccess ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
+                                        状态: ${node.testResults[0].isSuccess ? '成功' : '失败'}
+                                    </div>
+                                    ${node.testResults[0].isSuccess ? `
+                                        <div class="result-detail">
+                                            <i class="bi bi-speedometer2"></i>
+                                            延迟: ${node.testResults[0].latency}ms
+                                        </div>
+                                        <div class="result-detail">
+                                            <i class="bi bi-download"></i>
+                                            下载: ${formatSpeed(node.testResults[0].downloadSpeed)}
+                                        </div>
+                                        <div class="result-detail">
+                                            <i class="bi bi-upload"></i>
+                                            上传: ${formatSpeed(node.testResults[0].uploadSpeed)}
+                                        </div>
+                                    ` : `
+                                        <div class="result-detail">
+                                            <i class="bi bi-exclamation-triangle"></i>
+                                            错误: ${node.testResults[0].error || '未知错误'}
+                                        </div>
+                                    `}
+                                </div>
+                            </div>
+                        ` : '<div class="text-muted">暂无测速结果</div>'}
+                    </div>
+                </div>
+            `;
+            
+            modal.show();
+        }
+    } catch (error) {
+        console.error('获取节点详情失败:', error);
+        showError('获取节点详情失败: ' + error.message);
+    }
+}
+
+// 测试单个节点
+async function testSingleNode(nodeId) {
+    try {
+        showLoading();
+        
+        const response = await fetch(`${API_BASE}/test/manual`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({ nodeIds: [nodeId] })
+        });
+        
+        const data = await response.json();
+        
+        if (data.success) {
+            showSuccess('节点测速已开始');
+            // 刷新当前页面数据
+            if (currentPage === 'nodes') {
+                await loadNodes();
+            } else if (currentPage === 'speed-test') {
+                await loadTestResults();
+            }
+        } else {
+            showError('节点测速失败: ' + data.error);
+        }
+        
+        hideLoading();
+    } catch (error) {
+        console.error('节点测速失败:', error);
+        hideLoading();
+        showError('节点测速失败: ' + error.message);
+    }
+}
+
+// 更新分页
+function updatePagination(containerId, pagination, loadFunction) {
+    const container = document.getElementById(containerId);
+    
+    if (pagination.pages <= 1) {
+        container.innerHTML = '';
+        return;
+    }
+    
+    let html = '<ul class="pagination">';
+    
+    // 上一页
+    if (pagination.page > 1) {
+        html += `<li class="page-item"><a class="page-link" href="#" onclick="loadFunction(${pagination.page - 1})">上一页</a></li>`;
+    }
+    
+    // 页码
+    const startPage = Math.max(1, pagination.page - 2);
+    const endPage = Math.min(pagination.pages, pagination.page + 2);
+    
+    for (let i = startPage; i <= endPage; i++) {
+        html += `<li class="page-item ${i === pagination.page ? 'active' : ''}"><a class="page-link" href="#" onclick="loadFunction(${i})">${i}</a></li>`;
+    }
+    
+    // 下一页
+    if (pagination.page < pagination.pages) {
+        html += `<li class="page-item"><a class="page-link" href="#" onclick="loadFunction(${pagination.page + 1})">下一页</a></li>`;
+    }
+    
+    html += '</ul>';
+    container.innerHTML = html;
+}
+
+// 工具函数
+function formatTime(timeString) {
+    const date = new Date(timeString);
+    return date.toLocaleString('zh-CN');
+}
+
+function formatSpeed(speed) {
+    if (!speed) return 'N/A';
+    if (speed < 1024) return speed + ' B/s';
+    if (speed < 1024 * 1024) return (speed / 1024).toFixed(1) + ' KB/s';
+    if (speed < 1024 * 1024 * 1024) return (speed / (1024 * 1024)).toFixed(1) + ' MB/s';
+    return (speed / (1024 * 1024 * 1024)).toFixed(1) + ' GB/s';
+}
+
+function formatUptime(seconds) {
+    if (!seconds) return 'N/A';
+    const hours = Math.floor(seconds / 3600);
+    const minutes = Math.floor((seconds % 3600) / 60);
+    return `${hours}小时${minutes}分钟`;
+}
+
+function showLoading() {
+    document.getElementById('loading-overlay').style.display = 'flex';
+}
+
+function hideLoading() {
+    document.getElementById('loading-overlay').style.display = 'none';
+}
+
+function showSuccess(message) {
+    // 创建成功提示
+    const toast = document.createElement('div');
+    toast.className = 'alert alert-success alert-dismissible fade show position-fixed';
+    toast.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
+    toast.innerHTML = `
+        ${message}
+        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
+    `;
+    document.body.appendChild(toast);
+    
+    // 3秒后自动移除
+    setTimeout(() => {
+        if (toast.parentNode) {
+            toast.parentNode.removeChild(toast);
+        }
+    }, 3000);
+}
+
+function showError(message) {
+    // 创建错误提示
+    const toast = document.createElement('div');
+    toast.className = 'alert alert-danger alert-dismissible fade show position-fixed';
+    toast.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
+    toast.innerHTML = `
+        ${message}
+        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
+    `;
+    document.body.appendChild(toast);
+    
+    // 5秒后自动移除
+    setTimeout(() => {
+        if (toast.parentNode) {
+            toast.parentNode.removeChild(toast);
+        }
+    }, 5000);
+} 

+ 464 - 0
public/index.html

@@ -0,0 +1,464 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Clash Speed Test - 节点测速工具</title>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
+    <link href="styles.css" rel="stylesheet">
+</head>
+<body>
+    <!-- 导航栏 -->
+    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
+        <div class="container">
+            <a class="navbar-brand" href="#">
+                <i class="bi bi-speedometer2"></i>
+                Clash Speed Test
+            </a>
+            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarNav">
+                <ul class="navbar-nav me-auto">
+                    <li class="nav-item">
+                        <a class="nav-link active" href="#dashboard" data-page="dashboard">
+                            <i class="bi bi-house"></i> 仪表板
+                        </a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="#nodes" data-page="nodes">
+                            <i class="bi bi-hdd-network"></i> 节点管理
+                        </a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="#speed-test" data-page="speed-test">
+                            <i class="bi bi-speedometer"></i> 测速结果
+                        </a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="#notifications" data-page="notifications">
+                            <i class="bi bi-bell"></i> 通知记录
+                        </a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="#settings" data-page="settings">
+                            <i class="bi bi-gear"></i> 系统设置
+                        </a>
+                    </li>
+                </ul>
+                <div class="navbar-nav">
+                    <span class="navbar-text" id="status-indicator">
+                        <i class="bi bi-circle-fill text-success"></i> 系统正常
+                    </span>
+                </div>
+            </div>
+        </div>
+    </nav>
+
+    <!-- 主要内容区域 -->
+    <div class="container-fluid mt-4">
+        <!-- 仪表板页面 -->
+        <div id="dashboard-page" class="page-content">
+            <div class="row">
+                <div class="col-12">
+                    <h2><i class="bi bi-house"></i> 仪表板</h2>
+                    <p class="text-muted">系统概览和统计信息</p>
+                </div>
+            </div>
+
+            <!-- 统计卡片 -->
+            <div class="row mb-4">
+                <div class="col-md-3">
+                    <div class="card stat-card">
+                        <div class="card-body">
+                            <div class="d-flex justify-content-between">
+                                <div>
+                                    <h6 class="card-title text-muted">总节点数</h6>
+                                    <h3 class="mb-0" id="total-nodes">-</h3>
+                                </div>
+                                <div class="stat-icon">
+                                    <i class="bi bi-hdd-network text-primary"></i>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="card stat-card">
+                        <div class="card-body">
+                            <div class="d-flex justify-content-between">
+                                <div>
+                                    <h6 class="card-title text-muted">在线节点</h6>
+                                    <h3 class="mb-0" id="online-nodes">-</h3>
+                                </div>
+                                <div class="stat-icon">
+                                    <i class="bi bi-wifi text-success"></i>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="card stat-card">
+                        <div class="card-body">
+                            <div class="d-flex justify-content-between">
+                                <div>
+                                    <h6 class="card-title text-muted">测速成功率</h6>
+                                    <h3 class="mb-0" id="success-rate">-</h3>
+                                </div>
+                                <div class="stat-icon">
+                                    <i class="bi bi-graph-up text-info"></i>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="card stat-card">
+                        <div class="card-body">
+                            <div class="d-flex justify-content-between">
+                                <div>
+                                    <h6 class="card-title text-muted">今日测速</h6>
+                                    <h3 class="mb-0" id="today-tests">-</h3>
+                                </div>
+                                <div class="stat-icon">
+                                    <i class="bi bi-calendar-check text-warning"></i>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 快速操作 -->
+            <div class="row mb-4">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header">
+                            <h5><i class="bi bi-lightning"></i> 快速操作</h5>
+                        </div>
+                        <div class="card-body">
+                            <div class="row">
+                                <div class="col-md-3 mb-2">
+                                    <button class="btn btn-primary w-100" onclick="startSpeedTest()">
+                                        <i class="bi bi-speedometer"></i> 开始测速
+                                    </button>
+                                </div>
+                                <div class="col-md-3 mb-2">
+                                    <button class="btn btn-success w-100" onclick="testNotification()">
+                                        <i class="bi bi-bell"></i> 测试通知
+                                    </button>
+                                </div>
+                                <div class="col-md-3 mb-2">
+                                    <button class="btn btn-info w-100" onclick="refreshStats()">
+                                        <i class="bi bi-arrow-clockwise"></i> 刷新统计
+                                    </button>
+                                </div>
+                                <div class="col-md-3 mb-2">
+                                    <button class="btn btn-secondary w-100" onclick="showSystemStatus()">
+                                        <i class="bi bi-info-circle"></i> 系统状态
+                                    </button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 最近测速结果 -->
+            <div class="row">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header d-flex justify-content-between align-items-center">
+                            <h5><i class="bi bi-clock-history"></i> 最近测速结果</h5>
+                            <a href="#speed-test" class="btn btn-sm btn-outline-primary" data-page="speed-test">查看全部</a>
+                        </div>
+                        <div class="card-body">
+                            <div id="recent-results">
+                                <div class="text-center text-muted">
+                                    <i class="bi bi-hourglass-split"></i> 加载中...
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 节点管理页面 -->
+        <div id="nodes-page" class="page-content" style="display: none;">
+            <div class="row">
+                <div class="col-12">
+                    <h2><i class="bi bi-hdd-network"></i> 节点管理</h2>
+                    <p class="text-muted">管理所有代理节点</p>
+                </div>
+            </div>
+
+            <!-- 搜索和过滤 -->
+            <div class="row mb-4">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="row">
+                                <div class="col-md-4">
+                                    <input type="text" class="form-control" id="node-search" placeholder="搜索节点名称或服务器...">
+                                </div>
+                                <div class="col-md-2">
+                                    <select class="form-select" id="group-filter">
+                                        <option value="">所有分组</option>
+                                    </select>
+                                </div>
+                                <div class="col-md-2">
+                                    <select class="form-select" id="status-filter">
+                                        <option value="">所有状态</option>
+                                        <option value="online">在线</option>
+                                        <option value="offline">离线</option>
+                                    </select>
+                                </div>
+                                <div class="col-md-2">
+                                    <button class="btn btn-primary w-100" onclick="searchNodes()">
+                                        <i class="bi bi-search"></i> 搜索
+                                    </button>
+                                </div>
+                                <div class="col-md-2">
+                                    <button class="btn btn-success w-100" onclick="importNodes()">
+                                        <i class="bi bi-upload"></i> 导入节点
+                                    </button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 节点列表 -->
+            <div class="row">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header">
+                            <h5>节点列表</h5>
+                        </div>
+                        <div class="card-body">
+                            <div id="nodes-list">
+                                <div class="text-center text-muted">
+                                    <i class="bi bi-hourglass-split"></i> 加载中...
+                                </div>
+                            </div>
+                            <!-- 分页 -->
+                            <nav aria-label="节点分页" id="nodes-pagination">
+                            </nav>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 测速结果页面 -->
+        <div id="speed-test-page" class="page-content" style="display: none;">
+            <div class="row">
+                <div class="col-12">
+                    <h2><i class="bi bi-speedometer"></i> 测速结果</h2>
+                    <p class="text-muted">查看所有测速历史记录</p>
+                </div>
+            </div>
+
+            <!-- 过滤条件 -->
+            <div class="row mb-4">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="row">
+                                <div class="col-md-3">
+                                    <select class="form-select" id="result-node-filter">
+                                        <option value="">所有节点</option>
+                                    </select>
+                                </div>
+                                <div class="col-md-2">
+                                    <select class="form-select" id="result-status-filter">
+                                        <option value="">所有结果</option>
+                                        <option value="true">成功</option>
+                                        <option value="false">失败</option>
+                                    </select>
+                                </div>
+                                <div class="col-md-3">
+                                    <input type="date" class="form-control" id="start-date" placeholder="开始日期">
+                                </div>
+                                <div class="col-md-3">
+                                    <input type="date" class="form-control" id="end-date" placeholder="结束日期">
+                                </div>
+                                <div class="col-md-1">
+                                    <button class="btn btn-primary w-100" onclick="searchResults()">
+                                        <i class="bi bi-search"></i>
+                                    </button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 测速结果列表 -->
+            <div class="row">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header">
+                            <h5>测速历史</h5>
+                        </div>
+                        <div class="card-body">
+                            <div id="test-results">
+                                <div class="text-center text-muted">
+                                    <i class="bi bi-hourglass-split"></i> 加载中...
+                                </div>
+                            </div>
+                            <!-- 分页 -->
+                            <nav aria-label="结果分页" id="results-pagination">
+                            </nav>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 通知记录页面 -->
+        <div id="notifications-page" class="page-content" style="display: none;">
+            <div class="row">
+                <div class="col-12">
+                    <h2><i class="bi bi-bell"></i> 通知记录</h2>
+                    <p class="text-muted">查看所有Telegram通知记录</p>
+                </div>
+            </div>
+
+            <!-- 通知列表 -->
+            <div class="row">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header">
+                            <h5>通知历史</h5>
+                        </div>
+                        <div class="card-body">
+                            <div id="notifications-list">
+                                <div class="text-center text-muted">
+                                    <i class="bi bi-hourglass-split"></i> 加载中...
+                                </div>
+                            </div>
+                            <!-- 分页 -->
+                            <nav aria-label="通知分页" id="notifications-pagination">
+                            </nav>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 系统设置页面 -->
+        <div id="settings-page" class="page-content" style="display: none;">
+            <div class="row">
+                <div class="col-12">
+                    <h2><i class="bi bi-gear"></i> 系统设置</h2>
+                    <p class="text-muted">管理系统配置和状态</p>
+                </div>
+            </div>
+
+            <!-- 系统状态 -->
+            <div class="row mb-4">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header">
+                            <h5>系统状态</h5>
+                        </div>
+                        <div class="card-body">
+                            <div id="system-status">
+                                <div class="text-center text-muted">
+                                    <i class="bi bi-hourglass-split"></i> 加载中...
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 订阅管理 -->
+            <div class="row mb-4">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-header">
+                            <h5>订阅管理</h5>
+                        </div>
+                        <div class="card-body">
+                            <div class="row">
+                                <div class="col-md-6">
+                                    <h6>订阅状态</h6>
+                                    <div id="subscription-status">
+                                        <div class="text-muted">加载中...</div>
+                                    </div>
+                                </div>
+                                <div class="col-md-6">
+                                    <h6>操作</h6>
+                                    <button class="btn btn-primary" onclick="updateSubscription()">
+                                        <i class="bi bi-arrow-clockwise"></i> 更新订阅
+                                    </button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 模态框 -->
+    <!-- 节点详情模态框 -->
+    <div class="modal fade" id="nodeDetailModal" tabindex="-1">
+        <div class="modal-dialog modal-lg">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">节点详情</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                </div>
+                <div class="modal-body" id="node-detail-content">
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 导入节点模态框 -->
+    <div class="modal fade" id="importModal" tabindex="-1">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">导入节点</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                </div>
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <label class="form-label">配置文件路径</label>
+                        <input type="text" class="form-control" id="config-path" placeholder="./config/clash_config.yaml">
+                    </div>
+                    <div class="mb-3">
+                        <label class="form-label">或订阅URL</label>
+                        <input type="text" class="form-control" id="config-url" placeholder="https://example.com/config.yaml">
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
+                    <button type="button" class="btn btn-primary" onclick="confirmImport()">导入</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 加载提示 -->
+    <div id="loading-overlay" class="loading-overlay" style="display: none;">
+        <div class="loading-spinner">
+            <div class="spinner-border text-primary" role="status">
+                <span class="visually-hidden">加载中...</span>
+            </div>
+            <div class="mt-2">处理中,请稍候...</div>
+        </div>
+    </div>
+
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="app.js"></script>
+</body>
+</html> 

+ 389 - 0
public/styles.css

@@ -0,0 +1,389 @@
+/* 全局样式 */
+body {
+    background-color: #f8f9fa;
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+/* 导航栏样式 */
+.navbar-brand {
+    font-weight: 600;
+}
+
+.navbar-brand i {
+    margin-right: 8px;
+}
+
+/* 页面内容 */
+.page-content {
+    min-height: calc(100vh - 100px);
+}
+
+/* 统计卡片 */
+.stat-card {
+    border: none;
+    border-radius: 12px;
+    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+    transition: transform 0.2s ease;
+}
+
+.stat-card:hover {
+    transform: translateY(-2px);
+}
+
+.stat-icon {
+    font-size: 2rem;
+    opacity: 0.7;
+}
+
+/* 卡片样式 */
+.card {
+    border: none;
+    border-radius: 12px;
+    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+    margin-bottom: 20px;
+}
+
+.card-header {
+    background-color: #fff;
+    border-bottom: 1px solid #e9ecef;
+    border-radius: 12px 12px 0 0 !important;
+    font-weight: 600;
+}
+
+/* 节点列表样式 */
+.node-item {
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 15px;
+    margin-bottom: 10px;
+    background: white;
+    transition: all 0.2s ease;
+}
+
+.node-item:hover {
+    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+    transform: translateY(-1px);
+}
+
+.node-header {
+    display: flex;
+    justify-content: between;
+    align-items: center;
+    margin-bottom: 10px;
+}
+
+.node-name {
+    font-weight: 600;
+    color: #333;
+}
+
+.node-status {
+    padding: 4px 8px;
+    border-radius: 12px;
+    font-size: 0.8rem;
+    font-weight: 500;
+}
+
+.node-status.online {
+    background-color: #d4edda;
+    color: #155724;
+}
+
+.node-status.offline {
+    background-color: #f8d7da;
+    color: #721c24;
+}
+
+.node-info {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+    gap: 10px;
+    font-size: 0.9rem;
+    color: #666;
+}
+
+.node-info-item {
+    display: flex;
+    align-items: center;
+}
+
+.node-info-item i {
+    margin-right: 5px;
+    width: 16px;
+}
+
+/* 测速结果样式 */
+.result-item {
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 15px;
+    margin-bottom: 10px;
+    background: white;
+}
+
+.result-success {
+    border-left: 4px solid #28a745;
+}
+
+.result-failure {
+    border-left: 4px solid #dc3545;
+}
+
+.result-header {
+    display: flex;
+    justify-content: between;
+    align-items: center;
+    margin-bottom: 10px;
+}
+
+.result-node {
+    font-weight: 600;
+    color: #333;
+}
+
+.result-time {
+    color: #666;
+    font-size: 0.9rem;
+}
+
+.result-details {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+    gap: 10px;
+    font-size: 0.9rem;
+}
+
+.result-detail {
+    display: flex;
+    align-items: center;
+}
+
+.result-detail i {
+    margin-right: 5px;
+    width: 16px;
+}
+
+/* 通知样式 */
+.notification-item {
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    padding: 15px;
+    margin-bottom: 10px;
+    background: white;
+}
+
+.notification-sent {
+    border-left: 4px solid #28a745;
+}
+
+.notification-pending {
+    border-left: 4px solid #ffc107;
+}
+
+.notification-failed {
+    border-left: 4px solid #dc3545;
+}
+
+/* 按钮样式 */
+.btn {
+    border-radius: 8px;
+    font-weight: 500;
+    transition: all 0.2s ease;
+}
+
+.btn:hover {
+    transform: translateY(-1px);
+}
+
+.btn-primary {
+    background-color: #0d6efd;
+    border-color: #0d6efd;
+}
+
+.btn-success {
+    background-color: #198754;
+    border-color: #198754;
+}
+
+.btn-info {
+    background-color: #0dcaf0;
+    border-color: #0dcaf0;
+}
+
+.btn-warning {
+    background-color: #ffc107;
+    border-color: #ffc107;
+    color: #000;
+}
+
+.btn-danger {
+    background-color: #dc3545;
+    border-color: #dc3545;
+}
+
+/* 表单样式 */
+.form-control, .form-select {
+    border-radius: 8px;
+    border: 1px solid #ced4da;
+    transition: border-color 0.2s ease;
+}
+
+.form-control:focus, .form-select:focus {
+    border-color: #0d6efd;
+    box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
+}
+
+/* 分页样式 */
+.pagination {
+    justify-content: center;
+    margin-top: 20px;
+}
+
+.page-link {
+    border-radius: 6px;
+    margin: 0 2px;
+    border: 1px solid #dee2e6;
+}
+
+.page-link:hover {
+    background-color: #e9ecef;
+}
+
+.page-item.active .page-link {
+    background-color: #0d6efd;
+    border-color: #0d6efd;
+}
+
+/* 加载覆盖层 */
+.loading-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 9999;
+}
+
+.loading-spinner {
+    text-align: center;
+    color: white;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .node-info {
+        grid-template-columns: 1fr;
+    }
+    
+    .result-details {
+        grid-template-columns: 1fr;
+    }
+    
+    .stat-card {
+        margin-bottom: 15px;
+    }
+}
+
+/* 动画效果 */
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.fade-in {
+    animation: fadeIn 0.3s ease;
+}
+
+/* 状态指示器 */
+.status-indicator {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+}
+
+.status-indicator i {
+    font-size: 0.8rem;
+}
+
+/* 工具提示 */
+.tooltip {
+    font-size: 0.8rem;
+}
+
+/* 徽章样式 */
+.badge {
+    border-radius: 12px;
+    font-weight: 500;
+}
+
+/* 进度条样式 */
+.progress {
+    border-radius: 10px;
+    height: 8px;
+}
+
+.progress-bar {
+    border-radius: 10px;
+}
+
+/* 表格样式 */
+.table {
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.table th {
+    background-color: #f8f9fa;
+    border-bottom: 2px solid #dee2e6;
+    font-weight: 600;
+}
+
+.table td {
+    vertical-align: middle;
+}
+
+/* 模态框样式 */
+.modal-content {
+    border-radius: 12px;
+    border: none;
+    box-shadow: 0 10px 30px rgba(0,0,0,0.3);
+}
+
+.modal-header {
+    border-bottom: 1px solid #e9ecef;
+    border-radius: 12px 12px 0 0;
+}
+
+.modal-footer {
+    border-top: 1px solid #e9ecef;
+    border-radius: 0 0 12px 12px;
+}
+
+/* 自定义滚动条 */
+::-webkit-scrollbar {
+    width: 8px;
+}
+
+::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+} 

+ 15 - 0
public/sw.js

@@ -0,0 +1,15 @@
+// 简单的Service Worker,避免404错误
+self.addEventListener('install', function(event) {
+  // 跳过等待
+  self.skipWaiting();
+});
+
+self.addEventListener('activate', function(event) {
+  // 立即激活
+  event.waitUntil(self.clients.claim());
+});
+
+// 不处理任何请求,让浏览器使用默认行为
+self.addEventListener('fetch', function(event) {
+  // 不拦截任何请求
+}); 

+ 418 - 0
src/api/routes.js

@@ -0,0 +1,418 @@
+const express = require('express');
+const router = express.Router();
+const ClashParser = require('../core/clashParser');
+const SpeedTester = require('../core/speedTester');
+const TelegramNotifier = require('../core/notifier');
+const SubscriptionManager = require('../core/subscriptionManager');
+const { Node, TestResult, Notification } = require('../models');
+const logger = require('../utils/logger');
+
+// 节点管理API
+router.get('/nodes', async (req, res) => {
+  try {
+    const { page = 1, limit = 20, group, status, search } = req.query;
+    const offset = (page - 1) * limit;
+    
+    const where = {};
+    if (group) where.group = group;
+    if (status) where.status = status;
+    if (search) {
+      where[require('sequelize').Op.or] = [
+        { name: { [require('sequelize').Op.like]: `%${search}%` } },
+        { server: { [require('sequelize').Op.like]: `%${search}%` } }
+      ];
+    }
+
+    const { count, rows } = await Node.findAndCountAll({
+      where,
+      limit: parseInt(limit),
+      offset: parseInt(offset),
+      order: [['createdAt', 'DESC']],
+      include: [{
+        model: TestResult,
+        as: 'testResults',
+        limit: 1,
+        order: [['testTime', 'DESC']]
+      }]
+    });
+
+    res.json({
+      success: true,
+      data: {
+        nodes: rows,
+        pagination: {
+          page: parseInt(page),
+          limit: parseInt(limit),
+          total: count,
+          pages: Math.ceil(count / limit)
+        }
+      }
+    });
+  } catch (error) {
+    logger.error('获取节点列表失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.get('/nodes/:id', async (req, res) => {
+  try {
+    const node = await Node.findByPk(req.params.id, {
+      include: [{
+        model: TestResult,
+        as: 'testResults',
+        limit: 50,
+        order: [['testTime', 'DESC']]
+      }]
+    });
+
+    if (!node) {
+      return res.status(404).json({ success: false, error: '节点不存在' });
+    }
+
+    res.json({ success: true, data: node });
+  } catch (error) {
+    logger.error('获取节点详情失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.post('/nodes', async (req, res) => {
+  try {
+    const nodeData = req.body;
+    const node = await Node.create(nodeData);
+    
+    logger.info('创建节点成功', { nodeId: node.id, nodeName: node.name });
+    res.json({ success: true, data: node });
+  } catch (error) {
+    logger.error('创建节点失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.put('/nodes/:id', async (req, res) => {
+  try {
+    const node = await Node.findByPk(req.params.id);
+    if (!node) {
+      return res.status(404).json({ success: false, error: '节点不存在' });
+    }
+
+    await node.update(req.body);
+    
+    logger.info('更新节点成功', { nodeId: node.id, nodeName: node.name });
+    res.json({ success: true, data: node });
+  } catch (error) {
+    logger.error('更新节点失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.delete('/nodes/:id', async (req, res) => {
+  try {
+    const node = await Node.findByPk(req.params.id);
+    if (!node) {
+      return res.status(404).json({ success: false, error: '节点不存在' });
+    }
+
+    await node.destroy();
+    
+    logger.info('删除节点成功', { nodeId: node.id, nodeName: node.name });
+    res.json({ success: true });
+  } catch (error) {
+    logger.error('删除节点失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+// 导入Clash配置
+router.post('/import/clash', async (req, res) => {
+  try {
+    const { configPath, configUrl } = req.body;
+    
+    if (!configPath && !configUrl) {
+      return res.status(400).json({ success: false, error: '请提供配置文件路径或URL' });
+    }
+
+    const parser = new ClashParser(configPath || './temp_config.yaml');
+    let nodes;
+
+    if (configUrl) {
+      nodes = await parser.importFromUrl(configUrl);
+    } else {
+      nodes = await parser.parseConfig();
+    }
+
+    // 批量创建节点
+    const createdNodes = [];
+    for (const nodeData of nodes) {
+      try {
+        const existingNode = await Node.findOne({
+          where: {
+            name: nodeData.name,
+            server: nodeData.server,
+            port: nodeData.port
+          }
+        });
+
+        if (!existingNode) {
+          const node = await Node.create(nodeData);
+          createdNodes.push(node);
+        }
+      } catch (error) {
+        logger.warn('创建节点失败', { nodeData, error: error.message });
+      }
+    }
+
+    logger.info('导入Clash配置成功', { imported: createdNodes.length });
+    res.json({ 
+      success: true, 
+      data: { 
+        imported: createdNodes.length,
+        nodes: createdNodes 
+      } 
+    });
+  } catch (error) {
+    logger.error('导入Clash配置失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+// 订阅管理API
+router.post('/subscription/update', async (req, res) => {
+  try {
+    const subscriptionManager = new SubscriptionManager();
+    const result = await subscriptionManager.manualUpdate();
+    
+    if (result.success) {
+      res.json({ success: true, data: result.data });
+    } else {
+      res.status(400).json({ success: false, error: result.error });
+    }
+  } catch (error) {
+    logger.error('手动更新订阅失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.get('/subscription/status', async (req, res) => {
+  try {
+    const subscriptionManager = new SubscriptionManager();
+    const status = subscriptionManager.getStatus();
+    res.json({ success: true, data: status });
+  } catch (error) {
+    logger.error('获取订阅状态失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+// 测试相关API
+router.post('/test/manual', async (req, res) => {
+  try {
+    const { nodeIds } = req.body;
+    
+    let nodes;
+    if (nodeIds && nodeIds.length > 0) {
+      nodes = await Node.findAll({
+        where: { id: nodeIds }
+      });
+    } else {
+      nodes = await Node.findAll({
+        where: { isActive: true }
+      });
+    }
+
+    if (nodes.length === 0) {
+      return res.status(400).json({ success: false, error: '没有找到要测试的节点' });
+    }
+
+    const speedTester = new SpeedTester();
+    const results = await speedTester.testNodes(nodes);
+
+    logger.info('手动测试完成', { testedNodes: nodes.length });
+    res.json({ success: true, data: { results, testedNodes: nodes.length } });
+  } catch (error) {
+    logger.error('手动测试失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.get('/test/results', async (req, res) => {
+  try {
+    const { page = 1, limit = 50, nodeId, isSuccess, startDate, endDate } = req.query;
+    const offset = (page - 1) * limit;
+    
+    const where = {};
+    if (nodeId) where.nodeId = nodeId;
+    if (isSuccess !== undefined) where.isSuccess = isSuccess === 'true';
+    if (startDate || endDate) {
+      where.testTime = {};
+      if (startDate) where.testTime[require('sequelize').Op.gte] = new Date(startDate);
+      if (endDate) where.testTime[require('sequelize').Op.lte] = new Date(endDate);
+    }
+
+    const { count, rows } = await TestResult.findAndCountAll({
+      where,
+      limit: parseInt(limit),
+      offset: parseInt(offset),
+      order: [['testTime', 'DESC']],
+      include: [{
+        model: Node,
+        as: 'node',
+        attributes: ['name', 'type', 'group']
+      }]
+    });
+
+    res.json({
+      success: true,
+      data: {
+        results: rows,
+        pagination: {
+          page: parseInt(page),
+          limit: parseInt(limit),
+          total: count,
+          pages: Math.ceil(count / limit)
+        }
+      }
+    });
+  } catch (error) {
+    logger.error('获取测试结果失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+// 通知相关API
+router.get('/notifications', async (req, res) => {
+  try {
+    const { page = 1, limit = 20, type, isSent } = req.query;
+    const offset = (page - 1) * limit;
+    
+    const where = {};
+    if (type) where.type = type;
+    if (isSent !== undefined) where.isSent = isSent === 'true';
+
+    const { count, rows } = await Notification.findAndCountAll({
+      where,
+      limit: parseInt(limit),
+      offset: parseInt(offset),
+      order: [['createdAt', 'DESC']],
+      include: [{
+        model: Node,
+        as: 'node',
+        attributes: ['name', 'group']
+      }]
+    });
+
+    res.json({
+      success: true,
+      data: {
+        notifications: rows,
+        pagination: {
+          page: parseInt(page),
+          limit: parseInt(limit),
+          total: count,
+          pages: Math.ceil(count / limit)
+        }
+      }
+    });
+  } catch (error) {
+    logger.error('获取通知列表失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+router.post('/notifications/test', async (req, res) => {
+  try {
+    const notifier = new TelegramNotifier();
+    const result = await notifier.testConnection();
+    
+    if (result.success) {
+      res.json({ success: true, data: result });
+    } else {
+      res.status(400).json({ success: false, error: result.error });
+    }
+  } catch (error) {
+    logger.error('测试通知连接失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+// 统计信息API
+router.get('/stats', async (req, res) => {
+  try {
+    const [
+      totalNodes,
+      onlineNodes,
+      offlineNodes,
+      totalTests,
+      successfulTests,
+      totalNotifications
+    ] = await Promise.all([
+      Node.count(),
+      Node.count({ where: { status: 'online' } }),
+      Node.count({ where: { status: 'offline' } }),
+      TestResult.count(),
+      TestResult.count({ where: { isSuccess: true } }),
+      Notification.count()
+    ]);
+
+    // 获取最近24小时的测试统计
+    const yesterday = new Date();
+    yesterday.setDate(yesterday.getDate() - 1);
+    
+    const recentTests = await TestResult.count({
+      where: {
+        testTime: {
+          [require('sequelize').Op.gte]: yesterday
+        }
+      }
+    });
+
+    const recentSuccessfulTests = await TestResult.count({
+      where: {
+        testTime: {
+          [require('sequelize').Op.gte]: yesterday
+        },
+        isSuccess: true
+      }
+    });
+
+    const stats = {
+      nodes: {
+        total: totalNodes,
+        online: onlineNodes,
+        offline: offlineNodes,
+        onlineRate: totalNodes > 0 ? Math.round((onlineNodes / totalNodes) * 100) : 0
+      },
+      tests: {
+        total: totalTests,
+        successful: successfulTests,
+        successRate: totalTests > 0 ? Math.round((successfulTests / totalTests) * 100) : 0,
+        recent24h: recentTests,
+        recent24hSuccess: recentSuccessfulTests,
+        recent24hSuccessRate: recentTests > 0 ? Math.round((recentSuccessfulTests / recentTests) * 100) : 0
+      },
+      notifications: {
+        total: totalNotifications
+      }
+    };
+
+    res.json({ success: true, data: stats });
+  } catch (error) {
+    logger.error('获取统计信息失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+// 系统状态API
+router.get('/status', async (req, res) => {
+  try {
+    const scheduler = req.app.get('scheduler');
+    const status = scheduler ? scheduler.getStatus() : { error: '调度器未初始化' };
+    
+    res.json({ success: true, data: status });
+  } catch (error) {
+    logger.error('获取系统状态失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
+module.exports = router; 

+ 176 - 0
src/app.js

@@ -0,0 +1,176 @@
+const express = require('express');
+const cors = require('cors');
+const helmet = require('helmet');
+const compression = require('compression');
+require('dotenv').config();
+
+const logger = require('./utils/logger');
+const sequelize = require('./config/database');
+const routes = require('./api/routes');
+const Scheduler = require('./core/scheduler');
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+
+// 中间件
+app.use(helmet({
+  contentSecurityPolicy: {
+    directives: {
+      defaultSrc: ["'self'"],
+      styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
+      scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
+      scriptSrcAttr: ["'unsafe-inline'"],
+      fontSrc: ["'self'", "https://cdn.jsdelivr.net", "data:"],
+      imgSrc: ["'self'", "data:", "https:"],
+      mediaSrc: ["'self'", "data:"],
+      connectSrc: ["'self'"],
+      frameSrc: ["'none'"],
+      objectSrc: ["'none'"],
+      upgradeInsecureRequests: []
+    }
+  }
+}));
+app.use(compression());
+app.use(cors());
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// 静态文件服务
+app.use(express.static('public'));
+
+// 请求日志中间件 - 只记录重要请求
+app.use((req, res, next) => {
+  // 只记录API请求,不记录静态文件请求
+  if (req.path.startsWith('/api/') && !req.path.includes('/favicon') && !req.path.includes('/sw.js')) {
+    logger.info(`${req.method} ${req.path}`);
+  }
+  next();
+});
+
+// API路由
+app.use('/api', routes);
+
+// 健康检查
+app.get('/health', (req, res) => {
+  res.json({
+    status: 'ok',
+    timestamp: new Date().toISOString(),
+    uptime: process.uptime()
+  });
+});
+
+// 根路径
+app.get('/', (req, res) => {
+  res.json({
+    name: 'Clash Speed Test',
+    version: '1.0.0',
+    description: 'Clash节点测速工具,支持定时监控和Telegram通知',
+    endpoints: {
+      health: '/health',
+      api: '/api'
+    }
+  });
+});
+
+// 错误处理中间件
+app.use((err, req, res, next) => {
+  logger.error('未处理的错误', {
+    error: err.message,
+    stack: err.stack,
+    path: req.path,
+    method: req.method
+  });
+
+  res.status(500).json({
+    success: false,
+    error: process.env.NODE_ENV === 'production' ? '服务器内部错误' : err.message
+  });
+});
+
+// 404处理
+app.use('*', (req, res) => {
+  res.status(404).json({
+    success: false,
+    error: '接口不存在'
+  });
+});
+
+// 启动应用
+async function startApp() {
+  try {
+    // 测试数据库连接
+    await sequelize.authenticate();
+    logger.info('数据库连接成功');
+
+    // 同步数据库模型
+    await sequelize.sync({ alter: true });
+    logger.info('数据库模型同步完成');
+
+    // 启动调度器
+    const scheduler = new Scheduler();
+    scheduler.start();
+    app.set('scheduler', scheduler);
+
+    // 立即开始测速
+    setTimeout(async () => {
+      try {
+        logger.info('应用启动后立即开始测速');
+        await scheduler.runSpeedTest();
+      } catch (error) {
+        logger.error('启动测速失败', { error: error.message });
+      }
+    }, 3000); // 延迟3秒开始,确保应用完全启动
+
+    // 启动服务器
+    app.listen(PORT, () => {
+      logger.info(`服务器启动成功,端口: ${PORT}`);
+      logger.info(`健康检查: http://localhost:${PORT}/health`);
+      logger.info(`API文档: http://localhost:${PORT}/api`);
+    });
+
+  } catch (error) {
+    logger.error('应用启动失败', { error: error.message });
+    process.exit(1);
+  }
+}
+
+// 优雅关闭
+process.on('SIGTERM', async () => {
+  logger.info('收到SIGTERM信号,开始优雅关闭');
+  
+  const scheduler = app.get('scheduler');
+  if (scheduler) {
+    scheduler.stop();
+  }
+  
+  await sequelize.close();
+  logger.info('应用已关闭');
+  process.exit(0);
+});
+
+process.on('SIGINT', async () => {
+  logger.info('收到SIGINT信号,开始优雅关闭');
+  
+  const scheduler = app.get('scheduler');
+  if (scheduler) {
+    scheduler.stop();
+  }
+  
+  await sequelize.close();
+  logger.info('应用已关闭');
+  process.exit(0);
+});
+
+// 未捕获的异常处理
+process.on('uncaughtException', (error) => {
+  logger.error('未捕获的异常', { error: error.message, stack: error.stack });
+  process.exit(1);
+});
+
+process.on('unhandledRejection', (reason, promise) => {
+  logger.error('未处理的Promise拒绝', { reason, promise });
+  process.exit(1);
+});
+
+// 启动应用
+startApp(); 

+ 23 - 0
src/config/database.js

@@ -0,0 +1,23 @@
+const { Sequelize } = require('sequelize');
+require('dotenv').config();
+
+const sequelize = new Sequelize(
+  process.env.DB_NAME,
+  process.env.DB_USER,
+  process.env.DB_PASSWORD,
+  {
+    host: process.env.DB_HOST,
+    port: process.env.DB_PORT,
+    dialect: 'mysql',
+    logging: false, // 关闭SQL查询日志
+    pool: {
+      max: 5,
+      min: 0,
+      acquire: 30000,
+      idle: 10000
+    },
+    timezone: '+08:00'
+  }
+);
+
+module.exports = sequelize; 

+ 225 - 0
src/core/clashParser.js

@@ -0,0 +1,225 @@
+const fs = require('fs');
+const path = require('path');
+const yaml = require('yaml');
+const logger = require('../utils/logger');
+
+class ClashParser {
+  constructor(configPath) {
+    this.configPath = configPath;
+  }
+
+  /**
+   * 解析Clash配置文件
+   */
+  async parseConfig() {
+    try {
+      if (!fs.existsSync(this.configPath)) {
+        throw new Error(`配置文件不存在: ${this.configPath}`);
+      }
+
+      const configContent = fs.readFileSync(this.configPath, 'utf8');
+      const config = yaml.parse(configContent);
+
+      if (!config.proxies || !Array.isArray(config.proxies)) {
+        throw new Error('配置文件中没有找到有效的代理节点');
+      }
+
+      const nodes = [];
+      for (const proxy of config.proxies) {
+        const node = this.parseProxy(proxy);
+        if (node) {
+          nodes.push(node);
+        }
+      }
+
+      logger.info(`成功解析 ${nodes.length} 个节点`, { configPath: this.configPath });
+      return nodes;
+    } catch (error) {
+      logger.error('解析Clash配置文件失败', { error: error.message, configPath: this.configPath });
+      throw error;
+    }
+  }
+
+  /**
+   * 解析单个代理节点
+   */
+  parseProxy(proxy) {
+    try {
+      const baseNode = {
+        name: proxy.name,
+        type: proxy.type,
+        server: proxy.server,
+        port: parseInt(proxy.port),
+        group: proxy.group || 'default'
+      };
+
+      switch (proxy.type) {
+        case 'ss':
+          return this.parseShadowsocks(proxy, baseNode);
+        case 'ssr':
+          return this.parseShadowsocksR(proxy, baseNode);
+        case 'vmess':
+          return this.parseVmess(proxy, baseNode);
+        case 'trojan':
+          return this.parseTrojan(proxy, baseNode);
+        case 'http':
+        case 'https':
+          return this.parseHttp(proxy, baseNode);
+        case 'socks5':
+          return this.parseSocks5(proxy, baseNode);
+        default:
+          logger.warn(`不支持的代理类型: ${proxy.type}`, { nodeName: proxy.name });
+          return null;
+      }
+    } catch (error) {
+      logger.error('解析代理节点失败', { error: error.message, proxy });
+      return null;
+    }
+  }
+
+  /**
+   * 解析Shadowsocks节点
+   */
+  parseShadowsocks(proxy, baseNode) {
+    return {
+      ...baseNode,
+      password: proxy.password,
+      method: proxy.cipher || 'aes-256-gcm'
+    };
+  }
+
+  /**
+   * 解析ShadowsocksR节点
+   */
+  parseShadowsocksR(proxy, baseNode) {
+    return {
+      ...baseNode,
+      password: proxy.password,
+      method: proxy.cipher || 'aes-256-gcm',
+      protocol: proxy.protocol || 'origin',
+      protocolParam: proxy['protocol-param'] || '',
+      obfs: proxy.obfs || 'plain',
+      obfsParam: proxy['obfs-param'] || ''
+    };
+  }
+
+  /**
+   * 解析Vmess节点
+   */
+  parseVmess(proxy, baseNode) {
+    const node = {
+      ...baseNode,
+      uuid: proxy.uuid,
+      alterId: parseInt(proxy.alterId) || 0,
+      network: proxy.network || 'tcp'
+    };
+
+    // 处理TLS配置
+    if (proxy.tls) {
+      node.tls = true;
+      node.sni = proxy.servername || proxy.sni;
+    }
+
+    // 处理WebSocket配置
+    if (proxy.network === 'ws') {
+      node.wsPath = proxy.path || '/';
+      if (proxy.headers) {
+        node.wsHeaders = JSON.stringify(proxy.headers);
+      }
+    }
+
+    // 处理HTTP/2配置
+    if (proxy.network === 'h2') {
+      node.h2Path = proxy.path || '/';
+      node.h2Host = proxy.host || proxy.server;
+    }
+
+    return node;
+  }
+
+  /**
+   * 解析Trojan节点
+   */
+  parseTrojan(proxy, baseNode) {
+    const node = {
+      ...baseNode,
+      password: proxy.password
+    };
+
+    // 处理TLS配置
+    if (proxy.tls) {
+      node.tls = true;
+      node.sni = proxy.sni || proxy.server;
+    }
+
+    return node;
+  }
+
+  /**
+   * 解析HTTP代理节点
+   */
+  parseHttp(proxy, baseNode) {
+    const node = {
+      ...baseNode,
+      username: proxy.username || '',
+      password: proxy.password || ''
+    };
+
+    // 处理TLS配置
+    if (proxy.tls) {
+      node.tls = true;
+      node.sni = proxy.sni || proxy.server;
+    }
+
+    return node;
+  }
+
+  /**
+   * 解析SOCKS5代理节点
+   */
+  parseSocks5(proxy, baseNode) {
+    return {
+      ...baseNode,
+      username: proxy.username || '',
+      password: proxy.password || ''
+    };
+  }
+
+  /**
+   * 从URL导入配置
+   */
+  async importFromUrl(url) {
+    try {
+      const axios = require('axios');
+      const response = await axios.get(url, {
+        timeout: 10000,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+        }
+      });
+
+      const configContent = response.data;
+      const config = yaml.parse(configContent);
+
+      if (!config.proxies || !Array.isArray(config.proxies)) {
+        throw new Error('URL配置中没有找到有效的代理节点');
+      }
+
+      const nodes = [];
+      for (const proxy of config.proxies) {
+        const node = this.parseProxy(proxy);
+        if (node) {
+          nodes.push(node);
+        }
+      }
+
+      logger.info(`成功从URL导入 ${nodes.length} 个节点`, { url });
+      return nodes;
+    } catch (error) {
+      logger.error('从URL导入配置失败', { error: error.message, url });
+      throw error;
+    }
+  }
+}
+
+module.exports = ClashParser; 

+ 310 - 0
src/core/notifier.js

@@ -0,0 +1,310 @@
+const TelegramBot = require('node-telegram-bot-api');
+const logger = require('../utils/logger');
+const { Notification } = require('../models');
+
+class TelegramNotifier {
+  constructor() {
+    this.botToken = process.env.TELEGRAM_BOT_TOKEN;
+    this.chatId = process.env.TELEGRAM_CHAT_ID;
+    
+    if (!this.botToken || !this.chatId) {
+      logger.warn('Telegram配置不完整,通知功能将被禁用');
+      this.enabled = false;
+      return;
+    }
+
+    try {
+      this.bot = new TelegramBot(this.botToken, { polling: false });
+      this.enabled = true;
+      logger.info('Telegram通知器初始化成功');
+    } catch (error) {
+      logger.error('Telegram通知器初始化失败', { error: error.message });
+      this.enabled = false;
+    }
+  }
+
+  /**
+   * 发送节点故障通知
+   */
+  async sendFailureNotification(node, testResult) {
+    if (!this.enabled) return;
+
+    const title = '🚨 节点故障通知';
+    const message = this.formatFailureMessage(node, testResult);
+    
+    await this.sendNotification({
+      type: 'failure',
+      title,
+      message,
+      nodeId: node.id,
+      metadata: {
+        nodeName: node.name,
+        errorMessage: testResult.errorMessage,
+        failureCount: node.failureCount
+      }
+    });
+  }
+
+  /**
+   * 发送节点恢复通知
+   */
+  async sendRecoveryNotification(node, testResult) {
+    if (!this.enabled) return;
+
+    const title = '✅ 节点恢复通知';
+    const message = this.formatRecoveryMessage(node, testResult);
+    
+    await this.sendNotification({
+      type: 'recovery',
+      title,
+      message,
+      nodeId: node.id,
+      metadata: {
+        nodeName: node.name,
+        latency: testResult.latency,
+        speed: testResult.downloadSpeed
+      }
+    });
+  }
+
+  /**
+   * 发送系统通知
+   */
+  async sendSystemNotification(title, message, metadata = {}) {
+    if (!this.enabled) return;
+
+    await this.sendNotification({
+      type: 'system',
+      title,
+      message,
+      nodeId: null,
+      metadata
+    });
+  }
+
+  /**
+   * 发送测试摘要通知
+   */
+  async sendSummaryNotification(summary) {
+    if (!this.enabled) return;
+
+    const title = '📊 节点测试摘要';
+    const message = this.formatSummaryMessage(summary);
+    
+    await this.sendNotification({
+      type: 'summary',
+      title,
+      message,
+      nodeId: null,
+      metadata: summary
+    });
+  }
+
+  /**
+   * 发送通知
+   */
+  async sendNotification(notificationData) {
+    try {
+      // 保存通知记录
+      const notification = await Notification.create(notificationData);
+
+      // 发送Telegram消息
+      const fullMessage = `*${notificationData.title}*\n\n${notificationData.message}`;
+      
+      const sentMessage = await this.bot.sendMessage(this.chatId, fullMessage, {
+        parse_mode: 'Markdown',
+        disable_web_page_preview: true
+      });
+
+      // 更新通知状态
+      await notification.update({
+        isSent: true,
+        sentTime: new Date()
+      });
+
+      logger.info('Telegram通知发送成功', {
+        notificationId: notification.id,
+        messageId: sentMessage.message_id
+      });
+
+    } catch (error) {
+      logger.error('Telegram通知发送失败', {
+        error: error.message,
+        notificationData
+      });
+
+      // 更新通知状态
+      if (notificationData.id) {
+        await Notification.update({
+          retryCount: notificationData.retryCount + 1,
+          errorMessage: error.message
+        }, {
+          where: { id: notificationData.id }
+        });
+      }
+    }
+  }
+
+  /**
+   * 格式化故障消息
+   */
+  formatFailureMessage(node, testResult) {
+    const lines = [
+      `*节点名称:* ${node.name}`,
+      `*节点类型:* ${node.type}`,
+      `*服务器:* ${node.server}:${node.port}`,
+      `*分组:* ${node.group || '默认'}`,
+      `*连续失败次数:* ${node.failureCount}`,
+      `*最后测试时间:* ${new Date().toLocaleString('zh-CN')}`
+    ];
+
+    if (testResult.errorMessage) {
+      lines.push(`*错误信息:* ${testResult.errorMessage}`);
+    }
+
+    if (testResult.testDuration) {
+      lines.push(`*测试耗时:* ${testResult.testDuration}ms`);
+    }
+
+    return lines.join('\n');
+  }
+
+  /**
+   * 格式化恢复消息
+   */
+  formatRecoveryMessage(node, testResult) {
+    const lines = [
+      `*节点名称:* ${node.name}`,
+      `*节点类型:* ${node.type}`,
+      `*服务器:* ${node.server}:${node.port}`,
+      `*分组:* ${node.group || '默认'}`,
+      `*恢复时间:* ${new Date().toLocaleString('zh-CN')}`
+    ];
+
+    if (testResult.latency) {
+      lines.push(`*延迟:* ${testResult.latency}ms`);
+    }
+
+    if (testResult.downloadSpeed) {
+      lines.push(`*下载速度:* ${testResult.downloadSpeed} Mbps`);
+    }
+
+    if (testResult.ipAddress && testResult.ipAddress !== 'unknown') {
+      lines.push(`*IP地址:* ${testResult.ipAddress}`);
+    }
+
+    return lines.join('\n');
+  }
+
+  /**
+   * 格式化摘要消息
+   */
+  formatSummaryMessage(summary) {
+    const lines = [
+      `*测试时间:* ${new Date().toLocaleString('zh-CN')}`,
+      `*总节点数:* ${summary.totalNodes}`,
+      `*在线节点:* ${summary.onlineNodes}`,
+      `*离线节点:* ${summary.offlineNodes}`,
+      `*测试成功率:* ${summary.successRate}%`
+    ];
+
+    if (summary.averageLatency) {
+      lines.push(`*平均延迟:* ${summary.averageLatency}ms`);
+    }
+
+    if (summary.averageSpeed) {
+      lines.push(`*平均速度:* ${summary.averageSpeed} Mbps`);
+    }
+
+    if (summary.failedNodes && summary.failedNodes.length > 0) {
+      lines.push(`\n*故障节点:*`);
+      summary.failedNodes.forEach(node => {
+        lines.push(`• ${node.name} (${node.group})`);
+      });
+    }
+
+    if (summary.bestNodes && summary.bestNodes.length > 0) {
+      lines.push(`\n*最佳节点:*`);
+      summary.bestNodes.slice(0, 3).forEach(node => {
+        lines.push(`• ${node.name} - ${node.latency}ms`);
+      });
+    }
+
+    return lines.join('\n');
+  }
+
+  /**
+   * 重试失败的通知
+   */
+  async retryFailedNotifications() {
+    try {
+      const failedNotifications = await Notification.findAll({
+        where: {
+          isSent: false,
+          retryCount: { [require('sequelize').Op.lt]: 3 }
+        },
+        order: [['createdAt', 'ASC']]
+      });
+
+      for (const notification of failedNotifications) {
+        await this.sendNotification({
+          id: notification.id,
+          type: notification.type,
+          title: notification.title,
+          message: notification.message,
+          nodeId: notification.nodeId,
+          retryCount: notification.retryCount
+        });
+
+        // 避免发送过快
+        await new Promise(resolve => setTimeout(resolve, 1000));
+      }
+
+      logger.info(`重试了 ${failedNotifications.length} 个失败的通知`);
+    } catch (error) {
+      logger.error('重试失败通知时出错', { error: error.message });
+    }
+  }
+
+  /**
+   * 测试连接
+   */
+  async testConnection() {
+    if (!this.enabled) {
+      return { success: false, error: '通知器未启用' };
+    }
+
+    try {
+      const me = await this.bot.getMe();
+      logger.info('Telegram连接测试成功', { botName: me.first_name });
+      return { success: true, botName: me.first_name };
+    } catch (error) {
+      logger.error('Telegram连接测试失败', { error: error.message });
+      return { success: false, error: error.message };
+    }
+  }
+
+  /**
+   * 获取通知统计
+   */
+  async getNotificationStats() {
+    try {
+      const stats = await Notification.findAll({
+        attributes: [
+          'type',
+          'isSent',
+          [require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count']
+        ],
+        group: ['type', 'isSent'],
+        raw: true
+      });
+
+      return stats;
+    } catch (error) {
+      logger.error('获取通知统计失败', { error: error.message });
+      return [];
+    }
+  }
+}
+
+module.exports = TelegramNotifier; 

+ 309 - 0
src/core/scheduler.js

@@ -0,0 +1,309 @@
+const cron = require('node-cron');
+const logger = require('../utils/logger');
+const SpeedTester = require('./speedTester');
+const TelegramNotifier = require('./notifier');
+const SubscriptionManager = require('./subscriptionManager');
+const { Node, TestResult } = require('../models');
+
+class Scheduler {
+  constructor() {
+    this.speedTester = new SpeedTester();
+    this.notifier = new TelegramNotifier();
+    this.subscriptionManager = new SubscriptionManager();
+    this.isRunning = false;
+    this.testInterval = parseInt(process.env.SPEED_TEST_INTERVAL) || 15; // 分钟
+    this.failureThreshold = parseInt(process.env.NOTIFICATION_FAILURE_THRESHOLD) || 3;
+    this.recoveryThreshold = parseInt(process.env.NOTIFICATION_RECOVERY_THRESHOLD) || 2;
+  }
+
+  /**
+   * 启动调度器
+   */
+  start() {
+    logger.info('启动定时任务调度器 - 定时测速已关闭');
+
+    // 暂时关闭定时测速
+    // const cronExpression = `*/${this.testInterval} * * * *`;
+    // this.speedTestJob = cron.schedule(cronExpression, async () => {
+    //   await this.runSpeedTest();
+    // }, {
+    //   scheduled: true,
+    //   timezone: 'Asia/Shanghai'
+    // });
+
+    // 每小时重试失败的通知
+    this.notificationRetryJob = cron.schedule('0 * * * *', async () => {
+      await this.notifier.retryFailedNotifications();
+    }, {
+      scheduled: true,
+      timezone: 'Asia/Shanghai'
+    });
+
+    // 每天凌晨2点清理旧数据
+    this.cleanupJob = cron.schedule('0 2 * * *', async () => {
+      await this.cleanupOldData();
+    }, {
+      scheduled: true,
+      timezone: 'Asia/Shanghai'
+    });
+
+    // 启动订阅自动更新
+    this.subscriptionManager.startAutoUpdate();
+
+    logger.info('定时任务调度器启动成功');
+  }
+
+  /**
+   * 停止调度器
+   */
+  stop() {
+    if (this.speedTestJob) {
+      this.speedTestJob.stop();
+    }
+    if (this.notificationRetryJob) {
+      this.notificationRetryJob.stop();
+    }
+    if (this.cleanupJob) {
+      this.cleanupJob.stop();
+    }
+    this.subscriptionManager.stopAutoUpdate();
+    logger.info('定时任务调度器已停止');
+  }
+
+  /**
+   * 运行速度测试
+   */
+  async runSpeedTest() {
+    if (this.isRunning) {
+      logger.warn('上一次测试还在运行中,跳过本次测试');
+      return;
+    }
+
+    this.isRunning = true;
+    const startTime = Date.now();
+
+    try {
+      // 获取所有启用的节点
+      const nodes = await Node.findAll({
+        where: { isActive: true },
+        order: [['group', 'ASC'], ['name', 'ASC']]
+      });
+
+      if (nodes.length === 0) {
+        logger.warn('没有找到启用的节点');
+        return;
+      }
+
+      logger.info(`找到 ${nodes.length} 个节点,开始测试`);
+
+      // 批量测试节点
+      const testResults = await this.speedTester.testNodes(nodes);
+
+      // 处理测试结果
+      await this.processTestResults(nodes, testResults);
+
+      // 生成并发送摘要
+      await this.sendTestSummary(nodes, testResults);
+
+      const duration = Date.now() - startTime;
+      logger.info(`定时测速完成 - ${nodes.length}个节点,${testResults.filter(r => r.isSuccess).length}个成功,耗时${duration}ms`);
+
+    } catch (error) {
+      logger.error('定时速度测试失败', { error: error.message });
+      
+      // 发送系统错误通知
+      await this.notifier.sendSystemNotification(
+        '⚠️ 系统错误通知',
+        `定时速度测试过程中发生错误:\n\n${error.message}`,
+        { error: error.message, timestamp: new Date().toISOString() }
+      );
+    } finally {
+      this.isRunning = false;
+    }
+  }
+
+  /**
+   * 处理测试结果
+   */
+  async processTestResults(nodes, testResults) {
+    const nodeMap = new Map(nodes.map(node => [node.id, node]));
+    const resultMap = new Map(testResults.map(result => [result.nodeId, result]));
+
+    for (const node of nodes) {
+      const testResult = resultMap.get(node.id);
+      if (!testResult) continue;
+
+      const previousStatus = node.status;
+      const previousFailureCount = node.failureCount;
+
+      // 更新节点状态
+      if (testResult.isSuccess) {
+        // 测试成功
+        if (node.status === 'offline') {
+          // 节点从离线恢复
+          if (previousFailureCount >= this.recoveryThreshold) {
+            await this.notifier.sendRecoveryNotification(node, testResult);
+          }
+        }
+
+        await node.update({
+          status: 'online',
+          lastTestTime: testResult.testTime,
+          lastTestResult: true,
+          failureCount: 0,
+          averageLatency: testResult.latency,
+          averageSpeed: testResult.downloadSpeed
+        });
+
+      } else {
+        // 测试失败
+        const newFailureCount = previousFailureCount + 1;
+        
+        await node.update({
+          status: 'offline',
+          lastTestTime: testResult.testTime,
+          lastTestResult: false,
+          failureCount: newFailureCount
+        });
+
+        // 检查是否需要发送故障通知
+        if (newFailureCount === this.failureThreshold && previousStatus === 'online') {
+          await this.notifier.sendFailureNotification(node, testResult);
+        }
+      }
+    }
+  }
+
+  /**
+   * 发送测试摘要
+   */
+  async sendTestSummary(nodes, testResults) {
+    const totalNodes = nodes.length;
+    const onlineNodes = testResults.filter(r => r.isSuccess).length;
+    const offlineNodes = totalNodes - onlineNodes;
+    const successRate = totalNodes > 0 ? Math.round((onlineNodes / totalNodes) * 100) : 0;
+
+    // 计算平均延迟和速度
+    const successfulResults = testResults.filter(r => r.isSuccess && r.latency);
+    const averageLatency = successfulResults.length > 0 
+      ? Math.round(successfulResults.reduce((sum, r) => sum + r.latency, 0) / successfulResults.length)
+      : null;
+
+    const speedResults = testResults.filter(r => r.isSuccess && r.downloadSpeed);
+    const averageSpeed = speedResults.length > 0
+      ? Math.round((speedResults.reduce((sum, r) => sum + r.downloadSpeed, 0) / speedResults.length) * 100) / 100
+      : null;
+
+    // 获取故障节点列表
+    const failedNodes = nodes.filter(node => {
+      const result = testResults.find(r => r.nodeId === node.id);
+      return result && !result.isSuccess;
+    }).slice(0, 5); // 最多显示5个
+
+    // 获取最佳节点列表
+    const bestNodes = testResults
+      .filter(r => r.isSuccess && r.latency)
+      .sort((a, b) => a.latency - b.latency)
+      .slice(0, 3)
+      .map(result => {
+        const node = nodes.find(n => n.id === result.nodeId);
+        return {
+          name: node ? node.name : 'Unknown',
+          latency: result.latency,
+          group: node ? node.group : 'Unknown'
+        };
+      });
+
+    const summary = {
+      totalNodes,
+      onlineNodes,
+      offlineNodes,
+      successRate,
+      averageLatency,
+      averageSpeed,
+      failedNodes,
+      bestNodes,
+      testTime: new Date().toLocaleString('zh-CN')
+    };
+
+    // 只在有故障节点或成功率较低时发送摘要
+    if (failedNodes.length > 0 || successRate < 80) {
+      await this.notifier.sendSummaryNotification(summary);
+    }
+
+    logger.info('测试摘要生成完成', summary);
+  }
+
+  /**
+   * 清理旧数据
+   */
+  async cleanupOldData() {
+    try {
+      logger.info('开始清理旧数据');
+
+      // 删除30天前的测试结果
+      const thirtyDaysAgo = new Date();
+      thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+      const deletedTestResults = await TestResult.destroy({
+        where: {
+          testTime: {
+            [require('sequelize').Op.lt]: thirtyDaysAgo
+          }
+        }
+      });
+
+      // 删除7天前的通知记录
+      const sevenDaysAgo = new Date();
+      sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
+
+      const deletedNotifications = await require('../models').Notification.destroy({
+        where: {
+          createdAt: {
+            [require('sequelize').Op.lt]: sevenDaysAgo
+          },
+          isSent: true
+        }
+      });
+
+      logger.info('旧数据清理完成', {
+        deletedTestResults,
+        deletedNotifications
+      });
+
+    } catch (error) {
+      logger.error('清理旧数据失败', { error: error.message });
+    }
+  }
+
+  /**
+   * 手动触发测试
+   */
+  async triggerManualTest() {
+    if (this.isRunning) {
+      throw new Error('测试正在进行中,请稍后再试');
+    }
+
+    logger.info('手动触发速度测试');
+    await this.runSpeedTest();
+  }
+
+  /**
+   * 获取调度器状态
+   */
+  getStatus() {
+    return {
+      isRunning: this.isRunning,
+      testInterval: this.testInterval,
+      failureThreshold: this.failureThreshold,
+      recoveryThreshold: this.recoveryThreshold,
+      jobs: {
+        speedTest: this.speedTestJob ? 'running' : 'stopped',
+        notificationRetry: this.notificationRetryJob ? 'running' : 'stopped',
+        cleanup: this.cleanupJob ? 'running' : 'stopped'
+      }
+    };
+  }
+}
+
+module.exports = Scheduler; 

+ 517 - 0
src/core/speedTester.js

@@ -0,0 +1,517 @@
+const axios = require('axios');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const { SocksProxyAgent } = require('socks-proxy-agent');
+const { HttpProxyAgent } = require('http-proxy-agent');
+const logger = require('../utils/logger');
+const { TestResult } = require('../models');
+
+class SpeedTester {
+  constructor() {
+    this.testUrls = process.env.SPEED_TEST_URLS?.split(',') || [
+      'https://www.google.com',
+      'https://www.youtube.com',
+      'https://www.github.com'
+    ];
+    this.timeout = parseInt(process.env.SPEED_TEST_TIMEOUT) || 10000;
+  }
+
+  /**
+   * 测试单个节点
+   */
+  async testNode(node) {
+    const startTime = Date.now();
+    let testResult = {
+      nodeId: node.id,
+      testTime: new Date(),
+      isSuccess: false,
+      latency: null,
+      downloadSpeed: null,
+      uploadSpeed: null,
+      packetLoss: null,
+      testUrl: null,
+      errorMessage: null,
+      testDuration: null,
+      ipAddress: null,
+      location: null
+    };
+
+    try {
+      logger.info(`开始测试节点: ${node.name} (${node.type})`);
+
+      // 创建代理配置
+      const proxyConfig = this.createProxyConfig(node);
+      if (!proxyConfig) {
+        throw new Error(`不支持的代理类型: ${node.type}`);
+      }
+
+      // 简化测试流程:只测试基本连接性
+      const connectivityResult = await this.testBasicConnectivity(node, proxyConfig);
+      testResult.isSuccess = connectivityResult.success;
+      testResult.latency = connectivityResult.latency;
+      testResult.testUrl = connectivityResult.url;
+      testResult.ipAddress = connectivityResult.ipAddress;
+      testResult.location = connectivityResult.location;
+
+      testResult.testDuration = Date.now() - startTime;
+      
+      if (testResult.isSuccess) {
+        logger.info(`✅ 节点测试成功: ${node.name} - 延迟: ${testResult.latency}ms`);
+      } else {
+        logger.warn(`❌ 节点测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
+      }
+
+    } catch (error) {
+      testResult.errorMessage = error.message;
+      testResult.testDuration = Date.now() - startTime;
+      logger.error(`❌ 节点测试异常: ${node.name} - ${error.message}`);
+    }
+
+    // 保存测试结果
+    await this.saveTestResult(testResult);
+
+    return testResult;
+  }
+
+  /**
+   * 创建代理配置
+   */
+  createProxyConfig(node) {
+    const baseConfig = {
+      host: node.server,
+      port: node.port,
+      timeout: this.timeout
+    };
+
+    switch (node.type) {
+      case 'ss':
+        return this.createShadowsocksConfig(node, baseConfig);
+      case 'ssr':
+        return this.createShadowsocksRConfig(node, baseConfig);
+      case 'vmess':
+        return this.createVmessConfig(node, baseConfig);
+      case 'trojan':
+        return this.createTrojanConfig(node, baseConfig);
+      case 'http':
+      case 'https':
+        return this.createHttpConfig(node, baseConfig);
+      case 'socks5':
+        return this.createSocks5Config(node, baseConfig);
+      default:
+        logger.warn(`不支持的代理类型: ${node.type} - ${node.name}`);
+        return null;
+    }
+  }
+
+  /**
+   * 创建Shadowsocks代理配置
+   */
+  createShadowsocksConfig(node, baseConfig) {
+    return {
+      ...baseConfig,
+      type: 'ss',
+      password: node.password,
+      method: node.cipher || node.method,
+      // 尝试使用HTTP代理模式
+      auth: node.username && node.password ? {
+        username: node.username,
+        password: node.password
+      } : null
+    };
+  }
+
+  /**
+   * 创建ShadowsocksR代理配置
+   */
+  createShadowsocksRConfig(node, baseConfig) {
+    return {
+      ...baseConfig,
+      type: 'ssr',
+      password: node.password,
+      method: node.cipher || node.method,
+      protocol: node.protocol,
+      obfs: node.obfs,
+      // 尝试使用HTTP代理模式
+      auth: node.username && node.password ? {
+        username: node.username,
+        password: node.password
+      } : null
+    };
+  }
+
+  /**
+   * 创建Vmess代理配置
+   */
+  createVmessConfig(node, baseConfig) {
+    return {
+      ...baseConfig,
+      type: 'vmess',
+      uuid: node.uuid,
+      alterId: node.alterId,
+      network: node.network,
+      tls: node.tls,
+      sni: node.sni,
+      wsPath: node.wsPath,
+      wsHeaders: node.wsHeaders ? JSON.parse(node.wsHeaders) : {},
+      // 尝试使用HTTP代理模式
+      auth: node.username && node.password ? {
+        username: node.username,
+        password: node.password
+      } : null
+    };
+  }
+
+  /**
+   * 创建Trojan代理配置
+   */
+  createTrojanConfig(node, baseConfig) {
+    return {
+      ...baseConfig,
+      type: 'trojan',
+      password: node.password,
+      tls: node.tls,
+      sni: node.sni,
+      // 尝试使用HTTP代理模式
+      auth: node.username && node.password ? {
+        username: node.username,
+        password: node.password
+      } : null
+    };
+  }
+
+  /**
+   * 创建HTTP代理配置
+   */
+  createHttpConfig(node, baseConfig) {
+    const config = {
+      ...baseConfig,
+      type: 'http'
+    };
+
+    if (node.username && node.password) {
+      config.auth = {
+        username: node.username,
+        password: node.password
+      };
+    }
+
+    if (node.tls) {
+      config.https = true;
+      config.sni = node.sni;
+    }
+
+    return config;
+  }
+
+  /**
+   * 创建SOCKS5代理配置
+   */
+  createSocks5Config(node, baseConfig) {
+    const config = {
+      ...baseConfig,
+      type: 'socks5'
+    };
+
+    if (node.username && node.password) {
+      config.auth = {
+        username: node.username,
+        password: node.password
+      };
+    }
+
+    return config;
+  }
+
+  /**
+   * 测试延迟
+   */
+  async testLatency(node, proxyConfig) {
+    try {
+      const startTime = Date.now();
+      
+      // 使用HEAD请求测试延迟
+      const response = await this.makeRequest(node, proxyConfig, {
+        method: 'HEAD',
+        url: this.testUrls[0],
+        timeout: 5000
+      });
+
+      const latency = Date.now() - startTime;
+      
+      logger.debug(`延迟测试: ${node.name} - ${latency}ms`);
+      
+      return {
+        latency: latency,
+        packetLoss: 0 // 简化实现,实际需要多次测试计算丢包率
+      };
+    } catch (error) {
+      logger.debug(`延迟测试失败: ${node.name} - ${error.message}`);
+      return {
+        latency: null,
+        packetLoss: 100
+      };
+    }
+  }
+
+  /**
+   * 测试基本连接性(简化版本)
+   */
+  async testBasicConnectivity(node, proxyConfig) {
+    const startTime = Date.now();
+    
+    try {
+      // 尝试连接Google
+      const response = await this.makeRequest(node, proxyConfig, {
+        method: 'HEAD',
+        url: 'https://www.google.com',
+        timeout: 10000,
+        maxRedirects: 3
+      });
+
+      const latency = Date.now() - startTime;
+      
+      return {
+        success: true,
+        latency: latency,
+        url: 'https://www.google.com',
+        ipAddress: this.extractIpAddress(response),
+        location: this.extractLocation(response)
+      };
+    } catch (error) {
+      // 如果Google失败,尝试其他URL
+    for (const url of this.testUrls) {
+      try {
+        const response = await this.makeRequest(node, proxyConfig, {
+            method: 'HEAD',
+          url: url,
+            timeout: 10000,
+          maxRedirects: 3
+        });
+
+          const latency = Date.now() - startTime;
+
+        return {
+          success: true,
+            latency: latency,
+          url: url,
+          ipAddress: this.extractIpAddress(response),
+          location: this.extractLocation(response)
+        };
+        } catch (innerError) {
+          logger.debug(`连接测试失败: ${node.name} - ${url} - ${innerError.message}`);
+        continue;
+      }
+    }
+
+    return {
+      success: false,
+        latency: null,
+      url: null,
+      ipAddress: null,
+      location: null
+    };
+    }
+  }
+
+  /**
+   * 测试下载速度
+   */
+  async testDownloadSpeed(node, proxyConfig) {
+    try {
+      // 测试下载速度(使用更大的文件以获得更准确的结果)
+      const downloadStart = Date.now();
+      const downloadResponse = await this.makeRequest(node, proxyConfig, {
+        method: 'GET',
+        url: 'https://httpbin.org/bytes/1048576', // 1MB测试文件
+        timeout: this.timeout,
+        responseType: 'arraybuffer'
+      });
+
+      const downloadDuration = Date.now() - downloadStart;
+      const downloadSize = downloadResponse.data.byteLength;
+      
+      // 计算下载速度 (bytes per second)
+      const downloadSpeedBps = downloadSize / (downloadDuration / 1000);
+      
+      // 转换为Mbps
+      const downloadSpeedMbps = (downloadSpeedBps * 8) / (1024 * 1024);
+
+      // 测试上传速度(简化实现,使用POST请求)
+      const uploadStart = Date.now();
+      const testData = Buffer.alloc(1024 * 1024); // 1MB测试数据
+      await this.makeRequest(node, proxyConfig, {
+        method: 'POST',
+        url: 'https://httpbin.org/post',
+        data: testData,
+        timeout: this.timeout,
+        headers: {
+          'Content-Type': 'application/octet-stream'
+        }
+      });
+      
+      const uploadDuration = Date.now() - uploadStart;
+      const uploadSpeedBps = testData.length / (uploadDuration / 1000);
+      const uploadSpeedMbps = (uploadSpeedBps * 8) / (1024 * 1024);
+
+      logger.debug(`速度测试: ${node.name} - 下载: ${Math.round(downloadSpeedMbps * 100) / 100}Mbps, 上传: ${Math.round(uploadSpeedMbps * 100) / 100}Mbps`);
+
+      return {
+        downloadSpeed: Math.round(downloadSpeedMbps * 100) / 100,
+        uploadSpeed: Math.round(uploadSpeedMbps * 100) / 100
+      };
+    } catch (error) {
+      logger.debug(`速度测试失败: ${node.name} - ${error.message}`);
+      return {
+        downloadSpeed: null,
+        uploadSpeed: null
+      };
+    }
+  }
+
+  /**
+   * 发送HTTP请求
+   */
+  async makeRequest(node, proxyConfig, requestConfig) {
+    const agent = this.createProxyAgent(node, proxyConfig);
+    
+    if (!agent) {
+      throw new Error(`无法创建代理Agent: ${proxyConfig.type}`);
+    }
+    
+    logger.debug(`使用代理: ${node.name} - ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`);
+    
+    const config = {
+      ...requestConfig,
+      httpsAgent: agent,
+      httpAgent: agent,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+        ...requestConfig.headers
+      }
+    };
+
+    return axios(config);
+  }
+
+  /**
+   * 创建代理Agent(增强版本)
+   */
+  createProxyAgent(node, proxyConfig) {
+    try {
+      logger.debug(`创建代理Agent: ${node.name} - ${proxyConfig.type}`);
+      
+      switch (proxyConfig.type) {
+        case 'http':
+        case 'https':
+          // HTTP/HTTPS代理
+          const httpProxyUrl = `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`;
+          const httpAgent = new HttpsProxyAgent(httpProxyUrl);
+          logger.debug(`HTTP代理创建成功: ${node.name} - ${httpProxyUrl}`);
+          return httpAgent;
+          
+        case 'socks5':
+          // SOCKS5代理
+          const socksProxyUrl = `socks5://${proxyConfig.host}:${proxyConfig.port}`;
+          const socksAgent = new SocksProxyAgent(socksProxyUrl);
+          logger.debug(`SOCKS5代理创建成功: ${node.name} - ${socksProxyUrl}`);
+          return socksAgent;
+          
+        case 'ss':
+        case 'ssr':
+        case 'vmess':
+        case 'trojan':
+          // 对于这些高级代理类型,我们尝试使用HTTP代理方式
+          // 这需要代理服务器支持HTTP代理模式
+          logger.info(`尝试HTTP代理模式: ${proxyConfig.type} - ${node.name}`);
+          
+          // 构建代理URL
+          let proxyUrl = `http://${proxyConfig.host}:${proxyConfig.port}`;
+          
+          // 如果有认证信息,添加到URL中
+          if (proxyConfig.auth) {
+            const { username, password } = proxyConfig.auth;
+            proxyUrl = `http://${username}:${password}@${proxyConfig.host}:${proxyConfig.port}`;
+          }
+          
+          const advancedAgent = new HttpsProxyAgent(proxyUrl);
+          logger.debug(`高级代理创建成功: ${node.name} - ${proxyUrl}`);
+          return advancedAgent;
+          
+        default:
+          logger.warn(`不支持的代理类型: ${proxyConfig.type} - ${node.name}`);
+    return null;
+      }
+      
+    } catch (error) {
+      logger.error(`创建代理Agent失败: ${node.name}`, { error: error.message, type: proxyConfig.type });
+      return null;
+    }
+  }
+
+  /**
+   * 提取IP地址
+   */
+  extractIpAddress(response) {
+    // 从响应头中提取IP地址
+    return response.headers['x-forwarded-for'] || 
+           response.headers['x-real-ip'] || 
+           'unknown';
+  }
+
+  /**
+   * 提取位置信息
+   */
+  extractLocation(response) {
+    // 从响应头中提取位置信息
+    return response.headers['cf-ray'] ? 'Cloudflare' : 'unknown';
+  }
+
+  /**
+   * 保存测试结果
+   */
+  async saveTestResult(testResult) {
+    try {
+      await TestResult.create(testResult);
+    } catch (error) {
+      logger.error('保存测试结果失败', { error: error.message, testResult });
+    }
+  }
+
+  /**
+   * 批量测试节点
+   */
+  async testNodes(nodes) {
+    const results = [];
+    
+    // 并发测试,但限制并发数
+    const concurrency = 5;
+    const chunks = this.chunkArray(nodes, concurrency);
+    
+    for (const chunk of chunks) {
+      const chunkPromises = chunk.map(node => this.testNode(node));
+      const chunkResults = await Promise.allSettled(chunkPromises);
+      
+      for (const result of chunkResults) {
+        if (result.status === 'fulfilled') {
+          results.push(result.value);
+        } else {
+          logger.error('节点测试失败', { error: result.reason });
+        }
+      }
+    }
+    
+    return results;
+  }
+
+  /**
+   * 数组分块
+   */
+  chunkArray(array, size) {
+    const chunks = [];
+    for (let i = 0; i < array.length; i += size) {
+      chunks.push(array.slice(i, i + size));
+    }
+    return chunks;
+  }
+}
+
+module.exports = SpeedTester; 

+ 235 - 0
src/core/subscriptionManager.js

@@ -0,0 +1,235 @@
+const axios = require('axios');
+const yaml = require('yaml');
+const logger = require('../utils/logger');
+const ClashParser = require('./clashParser');
+const { Node } = require('../models');
+
+class SubscriptionManager {
+  constructor() {
+    this.subscriptionUrl = process.env.CLASH_SUBSCRIPTION_URL;
+    this.updateInterval = parseInt(process.env.SUBSCRIPTION_UPDATE_INTERVAL) || 3600000; // 默认1小时
+  }
+
+  /**
+   * 从订阅地址获取配置
+   */
+  async fetchSubscription() {
+    if (!this.subscriptionUrl) {
+      throw new Error('未配置订阅地址');
+    }
+
+    try {
+      // 简化日志输出
+
+      const response = await axios.get(this.subscriptionUrl, {
+        timeout: 30000,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+        }
+      });
+
+      // 检查响应内容
+      if (!response.data) {
+        throw new Error('订阅地址返回空内容');
+      }
+
+      // 尝试解析为YAML
+      let config;
+      let rawContent = response.data;
+      
+      try {
+        config = yaml.parse(rawContent);
+      } catch (error) {
+        // 如果不是YAML,尝试Base64解码
+        try {
+          const decoded = Buffer.from(rawContent, 'base64').toString('utf8');
+          config = yaml.parse(decoded);
+        } catch (decodeError) {
+          // 如果Base64解码也失败,尝试直接解析原始内容
+          // 有些订阅可能包含多个Base64编码的配置,用换行符分隔
+          const lines = rawContent.split('\n').filter(line => line.trim());
+          let decodedContent = '';
+          
+          for (const line of lines) {
+            try {
+              const decoded = Buffer.from(line, 'base64').toString('utf8');
+              decodedContent += decoded + '\n';
+            } catch (e) {
+              // 如果某行不是Base64,跳过
+              continue;
+            }
+          }
+          
+          if (decodedContent) {
+            config = yaml.parse(decodedContent);
+          } else {
+          throw new Error('无法解析订阅内容,既不是有效的YAML也不是Base64编码');
+          }
+        }
+      }
+
+      if (!config.proxies || !Array.isArray(config.proxies)) {
+        throw new Error('订阅配置中没有找到有效的代理节点');
+      }
+
+      // 简化日志输出
+
+      return config;
+    } catch (error) {
+      logger.error('获取订阅配置失败', { 
+        error: error.message, 
+        url: this.subscriptionUrl 
+      });
+      throw error;
+    }
+  }
+
+  /**
+   * 更新节点列表
+   */
+  async updateNodes() {
+    try {
+      // 简化日志输出
+
+      // 获取订阅配置
+      const config = await this.fetchSubscription();
+      
+      // 解析节点
+      const parser = new ClashParser();
+      const newNodes = [];
+      
+      for (const proxy of config.proxies) {
+        const node = parser.parseProxy(proxy);
+        if (node) {
+          newNodes.push(node);
+        }
+      }
+
+      if (newNodes.length === 0) {
+        logger.warn('订阅中没有找到有效的节点');
+        return { updated: 0, added: 0, removed: 0 };
+      }
+
+      // 获取现有节点
+      const existingNodes = await Node.findAll({
+        where: { isActive: true }
+      });
+
+      const existingNodeMap = new Map();
+      existingNodes.forEach(node => {
+        const key = `${node.name}-${node.server}-${node.port}`;
+        existingNodeMap.set(key, node);
+      });
+
+      let added = 0;
+      let updated = 0;
+      let removed = 0;
+
+      // 处理新节点
+      for (const nodeData of newNodes) {
+        const key = `${nodeData.name}-${nodeData.server}-${nodeData.port}`;
+        const existingNode = existingNodeMap.get(key);
+
+        if (existingNode) {
+          // 更新现有节点
+          await existingNode.update({
+            ...nodeData,
+            updatedAt: new Date()
+          });
+          updated++;
+          existingNodeMap.delete(key);
+        } else {
+          // 添加新节点
+          await Node.create({
+            ...nodeData,
+            isActive: true,
+            status: 'offline'
+          });
+          added++;
+        }
+      }
+
+      // 标记不再存在的节点为非活跃
+      for (const [key, node] of existingNodeMap) {
+        await node.update({ isActive: false });
+        removed++;
+      }
+
+      logger.info(`节点列表更新完成 - 新增${added}个,更新${updated}个,移除${removed}个`);
+
+      return { updated: newNodes.length, added, updated, removed };
+    } catch (error) {
+      logger.error('更新节点列表失败', { error: error.message });
+      throw error;
+    }
+  }
+
+  /**
+   * 启动自动更新
+   */
+  startAutoUpdate() {
+    if (!this.subscriptionUrl) {
+      logger.warn('未配置订阅地址,跳过自动更新');
+      return;
+    }
+
+    logger.info('启动订阅自动更新', { 
+      interval: `${this.updateInterval / 1000}秒` 
+    });
+
+    // 立即执行一次更新
+    this.updateNodes().catch(error => {
+      logger.error('初始订阅更新失败', { error: error.message });
+    });
+
+    // 设置定时更新
+    this.updateTimer = setInterval(() => {
+      this.updateNodes().catch(error => {
+        logger.error('定时订阅更新失败', { error: error.message });
+      });
+    }, this.updateInterval);
+  }
+
+  /**
+   * 停止自动更新
+   */
+  stopAutoUpdate() {
+    if (this.updateTimer) {
+      clearInterval(this.updateTimer);
+      this.updateTimer = null;
+      logger.info('订阅自动更新已停止');
+    }
+  }
+
+  /**
+   * 手动更新
+   */
+  async manualUpdate() {
+    try {
+      const result = await this.updateNodes();
+      return {
+        success: true,
+        data: result
+      };
+    } catch (error) {
+      return {
+        success: false,
+        error: error.message
+      };
+    }
+  }
+
+  /**
+   * 获取订阅状态
+   */
+  getStatus() {
+    return {
+      subscriptionUrl: this.subscriptionUrl,
+      updateInterval: this.updateInterval,
+      autoUpdateEnabled: !!this.updateTimer,
+      lastUpdate: this.lastUpdateTime
+    };
+  }
+}
+
+module.exports = SubscriptionManager; 

+ 62 - 0
src/database/migrate.js

@@ -0,0 +1,62 @@
+const sequelize = require('../config/database');
+const { Node, TestResult, Notification } = require('../models');
+const logger = require('../utils/logger');
+
+async function migrate() {
+  try {
+    logger.info('开始数据库迁移');
+
+    // 同步所有模型
+    await sequelize.sync({ force: false, alter: true });
+    
+    logger.info('数据库迁移完成');
+
+    // 检查是否需要创建索引
+    await createIndexes();
+
+    logger.info('数据库迁移和索引创建完成');
+    process.exit(0);
+  } catch (error) {
+    logger.error('数据库迁移失败', { error: error.message });
+    process.exit(1);
+  }
+}
+
+async function createIndexes() {
+  try {
+    // 创建额外的索引以提高查询性能
+    const queries = [
+      // 测试结果索引
+      'CREATE INDEX IF NOT EXISTS idx_test_results_node_time ON test_results(nodeId, testTime DESC)',
+      'CREATE INDEX IF NOT EXISTS idx_test_results_success ON test_results(isSuccess, testTime DESC)',
+      
+      // 通知索引
+      'CREATE INDEX IF NOT EXISTS idx_notifications_type_time ON notifications(type, createdAt DESC)',
+      'CREATE INDEX IF NOT EXISTS idx_notifications_sent ON notifications(isSent, retryCount)',
+      
+      // 节点索引
+      'CREATE INDEX IF NOT EXISTS idx_nodes_status_group ON nodes(status, group)',
+      'CREATE INDEX IF NOT EXISTS idx_nodes_last_test ON nodes(lastTestTime DESC)'
+    ];
+
+    for (const query of queries) {
+      try {
+        await sequelize.query(query);
+      } catch (error) {
+        // 索引可能已存在,忽略错误
+        logger.debug('索引创建跳过', { query, error: error.message });
+      }
+    }
+
+    logger.info('索引创建完成');
+  } catch (error) {
+    logger.error('创建索引失败', { error: error.message });
+  }
+}
+
+// 如果直接运行此文件
+if (require.main === module) {
+  migrate();
+}
+
+module.exports = { migrate, createIndexes }; 

+ 136 - 0
src/models/Node.js

@@ -0,0 +1,136 @@
+const { DataTypes } = require('sequelize');
+const sequelize = require('../config/database');
+
+const Node = sequelize.define('Node', {
+  id: {
+    type: DataTypes.INTEGER,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  name: {
+    type: DataTypes.STRING(255),
+    allowNull: false,
+    comment: '节点名称'
+  },
+  type: {
+    type: DataTypes.STRING(50),
+    allowNull: false,
+    comment: '节点类型 (ss, ssr, vmess, trojan等)'
+  },
+  server: {
+    type: DataTypes.STRING(255),
+    allowNull: false,
+    comment: '服务器地址'
+  },
+  port: {
+    type: DataTypes.INTEGER,
+    allowNull: false,
+    comment: '端口号'
+  },
+  password: {
+    type: DataTypes.STRING(255),
+    allowNull: true,
+    comment: '密码'
+  },
+  method: {
+    type: DataTypes.STRING(50),
+    allowNull: true,
+    comment: '加密方法'
+  },
+  uuid: {
+    type: DataTypes.STRING(255),
+    allowNull: true,
+    comment: 'UUID (vmess使用)'
+  },
+  alterId: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    comment: 'Alter ID (vmess使用)'
+  },
+  network: {
+    type: DataTypes.STRING(20),
+    allowNull: true,
+    defaultValue: 'tcp',
+    comment: '网络类型 (tcp, ws, http等)'
+  },
+  wsPath: {
+    type: DataTypes.STRING(255),
+    allowNull: true,
+    comment: 'WebSocket路径'
+  },
+  wsHeaders: {
+    type: DataTypes.TEXT,
+    allowNull: true,
+    comment: 'WebSocket请求头 (JSON格式)'
+  },
+  tls: {
+    type: DataTypes.BOOLEAN,
+    allowNull: true,
+    defaultValue: false,
+    comment: '是否启用TLS'
+  },
+  sni: {
+    type: DataTypes.STRING(255),
+    allowNull: true,
+    comment: 'SNI域名'
+  },
+  group: {
+    type: DataTypes.STRING(100),
+    allowNull: true,
+    comment: '节点分组'
+  },
+  isActive: {
+    type: DataTypes.BOOLEAN,
+    allowNull: false,
+    defaultValue: true,
+    comment: '是否启用'
+  },
+  lastTestTime: {
+    type: DataTypes.DATE,
+    allowNull: true,
+    comment: '最后测试时间'
+  },
+  lastTestResult: {
+    type: DataTypes.BOOLEAN,
+    allowNull: true,
+    comment: '最后测试结果'
+  },
+  averageLatency: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    comment: '平均延迟 (毫秒)'
+  },
+  averageSpeed: {
+    type: DataTypes.FLOAT,
+    allowNull: true,
+    comment: '平均速度 (Mbps)'
+  },
+  failureCount: {
+    type: DataTypes.INTEGER,
+    allowNull: false,
+    defaultValue: 0,
+    comment: '连续失败次数'
+  },
+  status: {
+    type: DataTypes.ENUM('online', 'offline', 'testing'),
+    allowNull: false,
+    defaultValue: 'offline',
+    comment: '节点状态'
+  }
+}, {
+  tableName: 'nodes',
+  timestamps: true,
+  indexes: [
+    {
+      fields: ['group']
+    },
+    {
+      fields: ['status']
+    },
+    {
+      fields: ['isActive']
+    }
+  ]
+});
+
+module.exports = Node; 

+ 82 - 0
src/models/Notification.js

@@ -0,0 +1,82 @@
+const { DataTypes } = require('sequelize');
+const sequelize = require('../config/database');
+
+const Notification = sequelize.define('Notification', {
+  id: {
+    type: DataTypes.INTEGER,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  nodeId: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    comment: '节点ID (可选,系统通知为null)'
+  },
+  type: {
+    type: DataTypes.ENUM('failure', 'recovery', 'system', 'summary'),
+    allowNull: false,
+    comment: '通知类型'
+  },
+  title: {
+    type: DataTypes.STRING(255),
+    allowNull: false,
+    comment: '通知标题'
+  },
+  message: {
+    type: DataTypes.TEXT,
+    allowNull: false,
+    comment: '通知内容'
+  },
+  platform: {
+    type: DataTypes.STRING(50),
+    allowNull: false,
+    defaultValue: 'telegram',
+    comment: '通知平台'
+  },
+  isSent: {
+    type: DataTypes.BOOLEAN,
+    allowNull: false,
+    defaultValue: false,
+    comment: '是否已发送'
+  },
+  sentTime: {
+    type: DataTypes.DATE,
+    allowNull: true,
+    comment: '发送时间'
+  },
+  retryCount: {
+    type: DataTypes.INTEGER,
+    allowNull: false,
+    defaultValue: 0,
+    comment: '重试次数'
+  },
+  errorMessage: {
+    type: DataTypes.TEXT,
+    allowNull: true,
+    comment: '发送错误信息'
+  },
+  metadata: {
+    type: DataTypes.TEXT,
+    allowNull: true,
+    comment: '额外数据 (JSON格式)'
+  }
+}, {
+  tableName: 'notifications',
+  timestamps: true,
+  indexes: [
+    {
+      fields: ['nodeId']
+    },
+    {
+      fields: ['type']
+    },
+    {
+      fields: ['isSent']
+    },
+    {
+      fields: ['createdAt']
+    }
+  ]
+});
+
+module.exports = Notification; 

+ 87 - 0
src/models/TestResult.js

@@ -0,0 +1,87 @@
+const { DataTypes } = require('sequelize');
+const sequelize = require('../config/database');
+
+const TestResult = sequelize.define('TestResult', {
+  id: {
+    type: DataTypes.INTEGER,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  nodeId: {
+    type: DataTypes.INTEGER,
+    allowNull: false,
+    comment: '节点ID'
+  },
+  testTime: {
+    type: DataTypes.DATE,
+    allowNull: false,
+    defaultValue: DataTypes.NOW,
+    comment: '测试时间'
+  },
+  isSuccess: {
+    type: DataTypes.BOOLEAN,
+    allowNull: false,
+    comment: '测试是否成功'
+  },
+  latency: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    comment: '延迟 (毫秒)'
+  },
+  downloadSpeed: {
+    type: DataTypes.FLOAT,
+    allowNull: true,
+    comment: '下载速度 (Mbps)'
+  },
+  uploadSpeed: {
+    type: DataTypes.FLOAT,
+    allowNull: true,
+    comment: '上传速度 (Mbps)'
+  },
+  packetLoss: {
+    type: DataTypes.FLOAT,
+    allowNull: true,
+    comment: '丢包率 (%)'
+  },
+  testUrl: {
+    type: DataTypes.STRING(255),
+    allowNull: true,
+    comment: '测试URL'
+  },
+  errorMessage: {
+    type: DataTypes.TEXT,
+    allowNull: true,
+    comment: '错误信息'
+  },
+  testDuration: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    comment: '测试持续时间 (毫秒)'
+  },
+  ipAddress: {
+    type: DataTypes.STRING(45),
+    allowNull: true,
+    comment: '节点IP地址'
+  },
+  location: {
+    type: DataTypes.STRING(100),
+    allowNull: true,
+    comment: '节点位置'
+  }
+}, {
+  tableName: 'test_results',
+  timestamps: true,
+  indexes: [
+    {
+      fields: ['nodeId']
+    },
+    {
+      fields: ['testTime']
+    },
+    {
+      fields: ['isSuccess']
+    }
+  ]
+});
+
+module.exports = TestResult; 

+ 32 - 0
src/models/index.js

@@ -0,0 +1,32 @@
+const Node = require('./Node');
+const TestResult = require('./TestResult');
+const Notification = require('./Notification');
+
+// 定义模型关联关系
+Node.hasMany(TestResult, {
+  foreignKey: 'nodeId',
+  as: 'testResults',
+  onDelete: 'CASCADE'
+});
+
+TestResult.belongsTo(Node, {
+  foreignKey: 'nodeId',
+  as: 'node'
+});
+
+Node.hasMany(Notification, {
+  foreignKey: 'nodeId',
+  as: 'notifications',
+  onDelete: 'CASCADE'
+});
+
+Notification.belongsTo(Node, {
+  foreignKey: 'nodeId',
+  as: 'node'
+});
+
+module.exports = {
+  Node,
+  TestResult,
+  Notification
+}; 

+ 61 - 0
src/utils/logger.js

@@ -0,0 +1,61 @@
+const winston = require('winston');
+const path = require('path');
+require('dotenv').config();
+
+// 创建logs目录
+const fs = require('fs');
+const logsDir = path.join(__dirname, '../../logs');
+if (!fs.existsSync(logsDir)) {
+  fs.mkdirSync(logsDir, { recursive: true });
+}
+
+// 定义日志格式
+const logFormat = winston.format.combine(
+  winston.format.timestamp({
+    format: 'YYYY-MM-DD HH:mm:ss'
+  }),
+  winston.format.errors({ stack: true }),
+  winston.format.json()
+);
+
+// 控制台格式
+const consoleFormat = winston.format.combine(
+  winston.format.colorize(),
+  winston.format.timestamp({
+    format: 'YYYY-MM-DD HH:mm:ss'
+  }),
+  winston.format.printf(({ timestamp, level, message, ...meta }) => {
+    return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`;
+  })
+);
+
+// 创建logger实例
+const logger = winston.createLogger({
+  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
+  format: logFormat,
+  defaultMeta: { service: 'clash-speed-test' },
+  transports: [
+    // 错误日志文件
+    new winston.transports.File({
+      filename: path.join(logsDir, 'error.log'),
+      level: 'error',
+      maxsize: 5242880, // 5MB
+      maxFiles: 5
+    }),
+    // 所有日志文件
+    new winston.transports.File({
+      filename: path.join(logsDir, 'combined.log'),
+      maxsize: 5242880, // 5MB
+      maxFiles: 5
+    })
+  ]
+});
+
+// 开发环境下添加控制台输出
+if (process.env.NODE_ENV !== 'production') {
+  logger.add(new winston.transports.Console({
+    format: consoleFormat
+  }));
+}
+
+module.exports = logger; 

+ 112 - 0
start.js

@@ -0,0 +1,112 @@
+#!/usr/bin/env node
+
+const { spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+
+console.log('🚀 启动Clash节点测速工具...\n');
+
+// 检查环境变量文件
+const envFile = path.join(__dirname, '.env');
+if (!fs.existsSync(envFile)) {
+  console.log('⚠️  未找到.env文件,请先复制env.example为.env并配置相关参数');
+  console.log('   命令: cp env.example .env');
+  process.exit(1);
+}
+
+// 检查数据库连接
+async function checkDatabase() {
+  try {
+    const sequelize = require('./src/config/database');
+    await sequelize.authenticate();
+    console.log('✅ 数据库连接正常');
+    return true;
+  } catch (error) {
+    console.log('❌ 数据库连接失败:', error.message);
+    console.log('   请检查数据库配置和连接');
+    return false;
+  }
+}
+
+// 检查Telegram配置
+function checkTelegram() {
+  const botToken = process.env.TELEGRAM_BOT_TOKEN;
+  const chatId = process.env.TELEGRAM_CHAT_ID;
+  
+  if (!botToken || !chatId) {
+    console.log('⚠️  Telegram配置不完整,通知功能将被禁用');
+    console.log('   请配置TELEGRAM_BOT_TOKEN和TELEGRAM_CHAT_ID');
+    return false;
+  }
+  
+  console.log('✅ Telegram配置正常');
+  console.log(`   Bot Token: ${botToken.substring(0, 10)}...${botToken.substring(botToken.length - 10)}`);
+  console.log(`   Chat ID: ${chatId}`);
+  return true;
+}
+
+// 主启动函数
+async function start() {
+  try {
+    // 加载环境变量
+    require('dotenv').config();
+    
+    // 检查配置
+    const dbOk = await checkDatabase();
+    if (!dbOk) {
+      process.exit(1);
+    }
+    
+    checkTelegram();
+    
+    console.log('\n📊 系统信息:');
+    console.log(`   端口: ${process.env.PORT || 3000}`);
+    console.log(`   环境: ${process.env.NODE_ENV || 'development'}`);
+    console.log(`   测速间隔: ${process.env.SPEED_TEST_INTERVAL || 15}分钟`);
+    console.log(`   测试超时: ${process.env.SPEED_TEST_TIMEOUT || 10000}ms`);
+    console.log(`   订阅更新间隔: ${(process.env.SUBSCRIPTION_UPDATE_INTERVAL || 3600000) / 1000}秒`);
+    
+    console.log('\n🌐 API接口:');
+    console.log(`   健康检查: http://localhost:${process.env.PORT || 3000}/health`);
+    console.log(`   API文档: http://localhost:${process.env.PORT || 3000}/api`);
+    console.log(`   订阅状态: http://localhost:${process.env.PORT || 3000}/api/subscription/status`);
+    
+    console.log('\n🔄 启动应用...\n');
+    
+    // 启动应用
+    const app = spawn('node', ['src/app.js'], {
+      stdio: 'inherit',
+      cwd: __dirname
+    });
+    
+    app.on('error', (error) => {
+      console.error('❌ 启动失败:', error.message);
+      process.exit(1);
+    });
+    
+    app.on('exit', (code) => {
+      if (code !== 0) {
+        console.error(`❌ 应用异常退出,退出码: ${code}`);
+        process.exit(code);
+      }
+    });
+    
+  } catch (error) {
+    console.error('❌ 启动过程中发生错误:', error.message);
+    process.exit(1);
+  }
+}
+
+// 处理信号
+process.on('SIGINT', () => {
+  console.log('\n\n👋 正在关闭应用...');
+  process.exit(0);
+});
+
+process.on('SIGTERM', () => {
+  console.log('\n\n👋 正在关闭应用...');
+  process.exit(0);
+});
+
+// 启动
+start(); 

+ 157 - 0
test-advanced-proxy.js

@@ -0,0 +1,157 @@
+const axios = require('axios');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const { SocksProxyAgent } = require('socks-proxy-agent');
+
+// 测试配置
+const testConfig = {
+  // 替换为你的实际代理配置
+  ss: {
+    name: 'SS-Node-1',
+    type: 'ss',
+    server: '1.2.3.4',
+    port: 8388,
+    password: 'your_password',
+    cipher: 'aes-256-gcm'
+  },
+  vmess: {
+    name: 'VMess-Node-1',
+    type: 'vmess',
+    server: '9.10.11.12',
+    port: 443,
+    uuid: 'your-uuid-here'
+  },
+  trojan: {
+    name: 'Trojan-Node-1',
+    type: 'trojan',
+    server: '17.18.19.20',
+    port: 443,
+    password: 'your_password'
+  }
+};
+
+/**
+ * 创建代理Agent
+ */
+function createProxyAgent(node) {
+  try {
+    console.log(`创建代理Agent: ${node.name} - ${node.type}`);
+    
+    switch (node.type) {
+      case 'ss':
+      case 'ssr':
+      case 'vmess':
+      case 'trojan':
+        // 尝试使用HTTP代理模式
+        console.log(`尝试HTTP代理模式: ${node.type} - ${node.name}`);
+        
+        // 构建代理URL
+        let proxyUrl = `http://${node.server}:${node.port}`;
+        
+        // 如果有认证信息,添加到URL中
+        if (node.username && node.password) {
+          proxyUrl = `http://${node.username}:${node.password}@${node.server}:${node.port}`;
+        }
+        
+        const agent = new HttpsProxyAgent(proxyUrl);
+        console.log(`高级代理创建成功: ${node.name} - ${proxyUrl}`);
+        return agent;
+        
+      default:
+        console.log(`不支持的代理类型: ${node.type} - ${node.name}`);
+        return null;
+    }
+    
+  } catch (error) {
+    console.error(`创建代理Agent失败: ${node.name}`, { error: error.message, type: node.type });
+    return null;
+  }
+}
+
+/**
+ * 测试代理连接
+ */
+async function testProxy(node) {
+  console.log(`\n=== 测试代理: ${node.name} ===`);
+  
+  try {
+    const agent = createProxyAgent(node);
+    
+    if (!agent) {
+      console.log(`❌ 无法创建代理Agent: ${node.name}`);
+      return false;
+    }
+    
+    console.log(`🔗 开始测试连接...`);
+    
+    const startTime = Date.now();
+    const response = await axios({
+      method: 'GET',
+      url: 'https://httpbin.org/ip',
+      httpsAgent: agent,
+      httpAgent: agent,
+      timeout: 10000,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+      }
+    });
+    
+    const latency = Date.now() - startTime;
+    
+    console.log(`✅ 连接成功!`);
+    console.log(`📊 延迟: ${latency}ms`);
+    console.log(`🌐 IP地址: ${response.data.origin}`);
+    console.log(`📄 响应状态: ${response.status}`);
+    
+    return true;
+    
+  } catch (error) {
+    console.log(`❌ 连接失败: ${error.message}`);
+    
+    if (error.code === 'ECONNREFUSED') {
+      console.log(`💡 提示: 连接被拒绝,可能是代理服务器未运行或端口错误`);
+    } else if (error.code === 'ETIMEDOUT') {
+      console.log(`💡 提示: 连接超时,可能是网络问题或代理配置错误`);
+    } else if (error.code === 'ENOTFOUND') {
+      console.log(`💡 提示: 无法解析主机名,请检查服务器地址`);
+    }
+    
+    return false;
+  }
+}
+
+/**
+ * 主测试函数
+ */
+async function main() {
+  console.log('🚀 开始测试高级代理类型...\n');
+  
+  // 测试直接连接
+  console.log('=== 测试直接连接 ===');
+  try {
+    const directResponse = await axios.get('https://httpbin.org/ip', { timeout: 5000 });
+    console.log(`✅ 直接连接正常 - IP: ${directResponse.data.origin}`);
+  } catch (error) {
+    console.log(`❌ 直接连接失败: ${error.message}`);
+  }
+  
+  // 测试各种代理类型
+  const testNodes = [
+    testConfig.ss,
+    testConfig.vmess,
+    testConfig.trojan
+  ];
+  
+  for (const node of testNodes) {
+    await testProxy(node);
+  }
+  
+  console.log('\n🎯 测试完成!');
+  console.log('\n💡 提示:');
+  console.log('1. 如果所有代理都失败,请检查代理服务器是否正常运行');
+  console.log('2. 确保代理配置信息正确(服务器地址、端口、密码等)');
+  console.log('3. 某些代理类型可能需要特殊的客户端库支持');
+  console.log('4. 可以尝试使用Clash等代理客户端来验证代理是否可用');
+}
+
+// 运行测试
+main().catch(console.error); 

+ 23 - 0
test-direct-connection.js

@@ -0,0 +1,23 @@
+const axios = require('axios');
+
+async function testDirectConnection() {
+  console.log('🔍 测试直接连接...');
+  
+  try {
+    const response = await axios.get('https://www.google.com', {
+      timeout: 10000,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+      }
+    });
+    
+    console.log('✅ 直接连接成功');
+    console.log(`状态码: ${response.status}`);
+    console.log(`响应大小: ${response.data.length} 字符`);
+    
+  } catch (error) {
+    console.error('❌ 直接连接失败:', error.message);
+  }
+}
+
+testDirectConnection(); 

+ 79 - 0
test-simple-proxy.js

@@ -0,0 +1,79 @@
+const axios = require('axios');
+const { HttpsProxyAgent } = require('https-proxy-agent');
+const { SocksProxyAgent } = require('socks-proxy-agent');
+
+async function testDirectConnection() {
+  console.log('🔍 测试直接连接...');
+  try {
+    const response = await axios.get('https://httpbin.org/ip', {
+      timeout: 10000
+    });
+    console.log('✅ 直接连接成功:', response.data);
+    return true;
+  } catch (error) {
+    console.log('❌ 直接连接失败:', error.message);
+    return false;
+  }
+}
+
+async function testProxyConnection(proxyType, host, port, username = null, password = null) {
+  console.log(`🔍 测试${proxyType}代理: ${host}:${port}`);
+  
+  try {
+    let agent;
+    let proxyUrl;
+    
+    if (proxyType === 'http' || proxyType === 'https') {
+      proxyUrl = `${proxyType}://${host}:${port}`;
+      if (username && password) {
+        proxyUrl = `${proxyType}://${username}:${password}@${host}:${port}`;
+      }
+      agent = new HttpsProxyAgent(proxyUrl);
+    } else if (proxyType === 'socks5') {
+      proxyUrl = `socks5://${host}:${port}`;
+      if (username && password) {
+        proxyUrl = `socks5://${username}:${password}@${host}:${port}`;
+      }
+      agent = new SocksProxyAgent(proxyUrl);
+    } else {
+      console.log(`❌ 不支持的代理类型: ${proxyType}`);
+      return false;
+    }
+    
+    console.log(`使用代理URL: ${proxyUrl}`);
+    
+    const response = await axios.get('https://httpbin.org/ip', {
+      httpsAgent: agent,
+      httpAgent: agent,
+      timeout: 10000,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+      }
+    });
+    
+    console.log('✅ 代理连接成功:', response.data);
+    return true;
+  } catch (error) {
+    console.log('❌ 代理连接失败:', error.message);
+    return false;
+  }
+}
+
+async function main() {
+  console.log('🚀 开始代理连接测试...\n');
+  
+  // 测试直接连接
+  await testDirectConnection();
+  console.log('');
+  
+  // 测试HTTP代理(如果有的话)
+  // 这里可以添加你的代理服务器信息
+  // await testProxyConnection('http', 'proxy.example.com', 8080);
+  
+  // 测试SOCKS5代理(如果有的话)
+  // await testProxyConnection('socks5', 'socks.example.com', 1080);
+  
+  console.log('✅ 测试完成');
+}
+
+main().catch(console.error); 

+ 77 - 0
test-subscription-content.js

@@ -0,0 +1,77 @@
+const axios = require('axios');
+const yaml = require('yaml');
+
+async function testSubscription() {
+  const subscriptionUrl = 'http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c';
+  
+  try {
+    console.log('🔍 测试订阅URL...');
+    console.log(`URL: ${subscriptionUrl}\n`);
+    
+    const response = await axios.get(subscriptionUrl, {
+      timeout: 30000,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+      }
+    });
+
+    console.log('✅ 成功获取响应');
+    console.log(`状态码: ${response.status}`);
+    console.log(`内容长度: ${response.data.length} 字符`);
+    console.log(`内容类型: ${response.headers['content-type'] || '未知'}\n`);
+
+    // 显示前500个字符
+    console.log('📄 响应内容预览 (前500字符):');
+    console.log('='.repeat(50));
+    console.log(response.data.substring(0, 500));
+    console.log('='.repeat(50));
+    console.log();
+
+    // 尝试解析为YAML
+    try {
+      console.log('🔄 尝试解析为YAML...');
+      const config = yaml.parse(response.data);
+      
+      if (config.proxies && Array.isArray(config.proxies)) {
+        console.log(`✅ 成功解析YAML,找到 ${config.proxies.length} 个代理节点`);
+        
+        // 显示前几个节点的信息
+        console.log('\n📋 前3个节点信息:');
+        config.proxies.slice(0, 3).forEach((proxy, index) => {
+          console.log(`${index + 1}. ${proxy.name} (${proxy.type}) - ${proxy.server}:${proxy.port}`);
+        });
+      } else {
+        console.log('❌ YAML解析成功,但没有找到proxies数组');
+        console.log('配置键:', Object.keys(config));
+      }
+    } catch (yamlError) {
+      console.log('❌ YAML解析失败:', yamlError.message);
+      
+      // 尝试Base64解码
+      try {
+        console.log('\n🔄 尝试Base64解码...');
+        const decoded = Buffer.from(response.data, 'base64').toString('utf8');
+        console.log('✅ Base64解码成功');
+        console.log('解码后内容长度:', decoded.length);
+        
+        const config = yaml.parse(decoded);
+        if (config.proxies && Array.isArray(config.proxies)) {
+          console.log(`✅ 成功解析Base64解码后的YAML,找到 ${config.proxies.length} 个代理节点`);
+        } else {
+          console.log('❌ Base64解码后仍没有找到proxies数组');
+        }
+      } catch (base64Error) {
+        console.log('❌ Base64解码失败:', base64Error.message);
+      }
+    }
+
+  } catch (error) {
+    console.error('❌ 请求失败:', error.message);
+    if (error.response) {
+      console.error('状态码:', error.response.status);
+      console.error('响应头:', error.response.headers);
+    }
+  }
+}
+
+testSubscription(); 

+ 61 - 0
test-subscription.js

@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+
+require('dotenv').config();
+const SubscriptionManager = require('./src/core/subscriptionManager');
+const logger = require('./src/utils/logger');
+
+async function testSubscription() {
+  console.log('🧪 测试订阅功能...\n');
+
+  try {
+    const subscriptionManager = new SubscriptionManager();
+    
+    console.log('📋 订阅配置信息:');
+    console.log(`   订阅地址: ${subscriptionManager.subscriptionUrl}`);
+    console.log(`   更新间隔: ${subscriptionManager.updateInterval / 1000}秒\n`);
+
+    console.log('🔄 开始获取订阅配置...');
+    const config = await subscriptionManager.fetchSubscription();
+    
+    console.log('✅ 订阅配置获取成功!');
+    console.log(`   节点数量: ${config.proxies.length}`);
+    
+    console.log('\n📊 节点类型统计:');
+    const typeStats = {};
+    config.proxies.forEach(proxy => {
+      typeStats[proxy.type] = (typeStats[proxy.type] || 0) + 1;
+    });
+    
+    Object.entries(typeStats).forEach(([type, count]) => {
+      console.log(`   ${type}: ${count}个`);
+    });
+
+    console.log('\n🔍 前5个节点信息:');
+    config.proxies.slice(0, 5).forEach((proxy, index) => {
+      console.log(`   ${index + 1}. ${proxy.name} (${proxy.type}) - ${proxy.server}:${proxy.port}`);
+    });
+
+    console.log('\n🔄 开始更新节点列表...');
+    const result = await subscriptionManager.updateNodes();
+    
+    console.log('✅ 节点列表更新完成!');
+    console.log(`   总节点数: ${result.updated}`);
+    console.log(`   新增节点: ${result.added}`);
+    console.log(`   更新节点: ${result.updated - result.added}`);
+    console.log(`   移除节点: ${result.removed}`);
+
+    console.log('\n🎉 订阅功能测试完成!');
+
+  } catch (error) {
+    console.error('❌ 订阅测试失败:', error.message);
+    console.error('   详细错误:', error);
+    process.exit(1);
+  }
+}
+
+// 如果直接运行此文件
+if (require.main === module) {
+  testSubscription();
+}
+
+module.exports = { testSubscription }; 

+ 110 - 0
test-telegram.js

@@ -0,0 +1,110 @@
+#!/usr/bin/env node
+
+require('dotenv').config();
+const TelegramBot = require('node-telegram-bot-api');
+const logger = require('./src/utils/logger');
+
+async function testTelegram() {
+  console.log('🤖 测试Telegram机器人...\n');
+
+  const botToken = process.env.TELEGRAM_BOT_TOKEN;
+  const chatId = process.env.TELEGRAM_CHAT_ID;
+
+  if (!botToken) {
+    console.error('❌ 未配置TELEGRAM_BOT_TOKEN');
+    process.exit(1);
+  }
+
+  if (!chatId) {
+    console.error('❌ 未配置TELEGRAM_CHAT_ID');
+    console.log('\n📋 如何获取Chat ID:');
+    console.log('   1. 将机器人添加到群组或频道');
+    console.log('   2. 在群组中发送 /start');
+    console.log('   3. 访问: https://api.telegram.org/bot' + botToken + '/getUpdates');
+    console.log('   4. 查找 "chat" 对象中的 "id" 字段');
+    process.exit(1);
+  }
+
+  try {
+    console.log('📋 机器人配置信息:');
+    console.log(`   Bot Token: ${botToken.substring(0, 10)}...${botToken.substring(botToken.length - 10)}`);
+    console.log(`   Chat ID: ${chatId}\n`);
+
+    // 创建机器人实例
+    const bot = new TelegramBot(botToken, { polling: false });
+
+    console.log('🔄 测试机器人连接...');
+    const me = await bot.getMe();
+    console.log('✅ 机器人连接成功!');
+    console.log(`   机器人名称: ${me.first_name}`);
+    console.log(`   机器人用户名: @${me.username}`);
+    console.log(`   机器人ID: ${me.id}\n`);
+
+    console.log('🔄 测试发送消息...');
+    const testMessage = `🧪 测试消息
+
+时间: ${new Date().toLocaleString('zh-CN')}
+状态: 机器人连接正常
+功能: Clash节点测速工具
+
+如果您收到此消息,说明Telegram通知功能配置成功!`;
+
+    const sentMessage = await bot.sendMessage(chatId, testMessage, {
+      parse_mode: 'Markdown',
+      disable_web_page_preview: true
+    });
+
+    console.log('✅ 消息发送成功!');
+    console.log(`   消息ID: ${sentMessage.message_id}`);
+    console.log(`   发送时间: ${new Date(sentMessage.date * 1000).toLocaleString('zh-CN')}\n`);
+
+    console.log('🔄 测试格式化消息...');
+    const formattedMessage = `🚨 *节点故障通知*
+
+*节点名称:* 测试节点
+*节点类型:* ss
+*服务器:* 1.2.3.4:8388
+*分组:* 测试分组
+*连续失败次数:* 3
+*最后测试时间:* ${new Date().toLocaleString('zh-CN')}
+*错误信息:* 连接超时
+*测试耗时:* 5000ms`;
+
+    const formattedSentMessage = await bot.sendMessage(chatId, formattedMessage, {
+      parse_mode: 'Markdown',
+      disable_web_page_preview: true
+    });
+
+    console.log('✅ 格式化消息发送成功!');
+    console.log(`   消息ID: ${formattedSentMessage.message_id}\n`);
+
+    console.log('🎉 Telegram机器人测试完成!');
+    console.log('\n📝 测试结果:');
+    console.log('   ✅ 机器人连接正常');
+    console.log('   ✅ 消息发送功能正常');
+    console.log('   ✅ 格式化消息功能正常');
+    console.log('   ✅ 通知功能已就绪');
+
+  } catch (error) {
+    console.error('❌ Telegram测试失败:', error.message);
+    
+    if (error.response) {
+      console.error('   错误详情:', error.response.data);
+    }
+    
+    console.log('\n🔧 故障排除建议:');
+    console.log('   1. 检查Bot Token是否正确');
+    console.log('   2. 检查Chat ID是否正确');
+    console.log('   3. 确保机器人已添加到目标群组/频道');
+    console.log('   4. 确保机器人有发送消息权限');
+    
+    process.exit(1);
+  }
+}
+
+// 如果直接运行此文件
+if (require.main === module) {
+  testTelegram();
+}
+
+module.exports = { testTelegram };