From 152c18696720a6bac8da01e148f4b005945ff293 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 07:15:31 +0000
Subject: [PATCH 1/3] Initial plan
From 6d3c63fd9bfbbe0671085ba07b63588ccc31335c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 07:33:55 +0000
Subject: [PATCH 2/3] feat: Build complete AI model real-time monitoring system
- Backend: Node.js + Express + Socket.IO + better-sqlite3
- server.js: HTTP/WebSocket server, real-time metrics broadcast
- database.js: SQLite schema, 8 model seeds, P95 latency queries
- routes/api.js: REST API for models, metrics, history, alerts
- services/simulator.js: Per-model realistic request simulation
- services/alertManager.js: Error rate / latency / spike alerts
- Frontend: Single-page Grafana-style dark dashboard
- Chart.js line/doughnut/bar charts with real-time updates
- 8 model cards with sparklines and health indicators
- Live request log table and alert panel
- Socket.IO WebSocket real-time data binding
- Infrastructure: Dockerfile + docker-compose.yml
- Docs: Chinese README and project-plan.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: LLibra1 <195303685+LLibra1@users.noreply.github.com>
---
.gitignore | 5 +
README.md | 166 ++-
backend/.gitignore | 4 +
backend/Dockerfile | 7 +
backend/package-lock.json | 1905 ++++++++++++++++++++++++++
backend/package.json | 21 +
backend/src/database.js | 206 +++
backend/src/routes/api.js | 143 ++
backend/src/server.js | 146 ++
backend/src/services/alertManager.js | 136 ++
backend/src/services/simulator.js | 171 +++
docker-compose.yml | 19 +
docs/project-plan.md | 221 +++
frontend/index.html | 841 ++++++++++++
14 files changed, 3990 insertions(+), 1 deletion(-)
create mode 100644 .gitignore
create mode 100644 backend/.gitignore
create mode 100644 backend/Dockerfile
create mode 100644 backend/package-lock.json
create mode 100644 backend/package.json
create mode 100644 backend/src/database.js
create mode 100644 backend/src/routes/api.js
create mode 100644 backend/src/server.js
create mode 100644 backend/src/services/alertManager.js
create mode 100644 backend/src/services/simulator.js
create mode 100644 docker-compose.yml
create mode 100644 docs/project-plan.md
create mode 100644 frontend/index.html
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e4d8733
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+data/
+*.db
+*.db-shm
+*.db-wal
+.env
diff --git a/README.md b/README.md
index d82f603..4cd4799 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,165 @@
-# Watch-System
\ No newline at end of file
+# AI 模型监控系统 (Watch-System)
+
+> 实时监控开源 AI/ML 模型调用状态的可视化仪表板
+
+---
+
+## 📋 项目概述
+
+Watch-System 是一套针对开源 AI/ML 模型(如 LLaMA2、Mistral、CodeLlama、Phi-2、Gemma、Qwen、DeepSeek 等)的**实时监控平台**。系统通过模拟真实 API 调用,持续采集请求量、响应时间、Token 用量、错误率等核心指标,并以美观的 Grafana 风格仪表板实时展示。
+
+---
+
+## 🏗️ 系统架构
+
+```
+┌─────────────────────────────────────────────────┐
+│ 浏览器 (前端) │
+│ ┌───────────────────────────────────────────┐ │
+│ │ index.html (Chart.js + Socket.IO Client) │ │
+│ │ - 实时图表 - 模型卡片 - 告警面板 │ │
+│ └──────────────────┬────────────────────────┘ │
+└─────────────────────┼───────────────────────────┘
+ │ WebSocket / HTTP
+┌─────────────────────▼───────────────────────────┐
+│ Node.js 后端 (:3001) │
+│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
+│ │ Express │ │Socket.IO │ │ REST API │ │
+│ │ Server │ │ Server │ │ /api/* │ │
+│ └────┬─────┘ └────┬─────┘ └──────┬────────┘ │
+│ │ │ │ │
+│ ┌────▼──────────────▼───────────────▼────────┐ │
+│ │ Services Layer │ │
+│ │ ┌─────────────┐ ┌──────────────────┐ │ │
+│ │ │ Simulator │ │ AlertManager │ │ │
+│ │ │ (模型模拟器) │ │ (告警管理器) │ │ │
+│ │ └──────┬──────┘ └────────┬─────────┘ │ │
+│ └─────────┼───────────────────┼─────────────┘ │
+│ │ │ │
+│ ┌─────────▼───────────────────▼─────────────┐ │
+│ │ SQLite Database (better-sqlite3) │ │
+│ │ requests 表 | models 表 │ │
+│ └────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## ✨ 功能特性
+
+| 功能 | 说明 |
+|------|------|
+| 🔴 实时监控 | 通过 Socket.IO WebSocket 推送,毫秒级延迟更新 |
+| 📊 多维图表 | 请求量趋势折线图、请求分布饼图、响应时间柱状图 |
+| 🤖 多模型支持 | 同时监控 8 个主流开源模型 |
+| 🚨 智能告警 | 错误率超阈值、响应过慢、流量突刺自动告警 |
+| 📝 请求日志 | 滚动显示最新 20 条请求详情 |
+| 🌙 暗色主题 | Grafana/DataDog 风格深色仪表板 |
+| 🐳 Docker 部署 | 一键 docker-compose up 启动 |
+| 💾 持久化存储 | SQLite 本地存储历史数据 |
+
+---
+
+## 🚀 快速启动
+
+### 方式一:本地运行
+
+```bash
+# 1. 安装后端依赖
+cd backend
+npm install
+
+# 2. 启动后端服务
+npm start
+# 或开发模式(热重载)
+npm run dev
+
+# 3. 打开前端
+open ../frontend/index.html
+```
+
+### 方式二:Docker Compose
+
+```bash
+docker-compose up --build
+# 前端访问: http://localhost:3000
+# 后端 API: http://localhost:3001
+```
+
+### 环境变量
+
+| 变量 | 默认值 | 说明 |
+|------|--------|------|
+| PORT | 3001 | 后端监听端口 |
+| NODE_ENV | development | 运行环境 |
+| DB_PATH | ./data/watch.db | SQLite 数据库路径 |
+
+---
+
+## 📁 项目结构
+
+```
+Watch-System/
+├── README.md
+├── docker-compose.yml
+├── docs/
+│ └── project-plan.md
+├── frontend/
+│ └── index.html
+└── backend/
+ ├── Dockerfile
+ ├── package.json
+ └── src/
+ ├── server.js
+ ├── database.js
+ ├── routes/
+ │ └── api.js
+ └── services/
+ ├── simulator.js
+ └── alertManager.js
+```
+
+---
+
+## 📡 API 接口
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /api/models | 获取所有模型及实时状态 |
+| GET | /api/metrics | 获取系统总体指标 |
+| GET | /api/metrics/:model | 获取指定模型详细指标 |
+| GET | /api/history/:model | 获取模型最近请求历史 |
+| GET | /api/alerts | 获取当前活跃告警 |
+| POST | /api/simulate | 手动触发一次模拟请求 |
+
+---
+
+## 🔌 Socket.IO 事件
+
+| 事件 | 方向 | 说明 |
+|------|------|------|
+| request_completed | 服务端→客户端 | 单次请求完成 |
+| metrics_update | 服务端→客户端 | 全量指标推送(每5秒) |
+| alert_created | 服务端→客户端 | 新告警产生 |
+| alert_resolved | 服务端→客户端 | 告警已解除 |
+
+---
+
+## 🤖 监控模型列表
+
+| 模型 | 特征 | 响应时间 |
+|------|------|----------|
+| llama2-7b | 通用对话 | 800-2000ms |
+| llama2-13b | 大参数通用 | 1500-4000ms |
+| mistral-7b | 快速推理 | 400-1200ms |
+| codellama-7b | 代码生成 | 600-1800ms |
+| phi-2 | 轻量快速 | 300-900ms |
+| gemma-7b | Google 开源 | 500-1500ms |
+| qwen-7b | 多语言 | 400-1100ms |
+| deepseek-7b | 深度推理 | 600-1600ms |
+
+---
+
+## 📄 许可证
+
+MIT License
diff --git a/backend/.gitignore b/backend/.gitignore
new file mode 100644
index 0000000..fc4225b
--- /dev/null
+++ b/backend/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+data/
+*.db
+.env
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..09bf607
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:18-alpine
+WORKDIR /app
+COPY package*.json ./
+RUN npm install --omit=dev
+COPY . .
+EXPOSE 3001
+CMD ["node", "src/server.js"]
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000..b907922
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,1905 @@
+{
+ "name": "watch-system-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "watch-system-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "better-sqlite3": "^12.9.0",
+ "cors": "^2.8.6",
+ "dotenv": "^16.6.1",
+ "express": "^4.22.1",
+ "socket.io": "^4.8.3",
+ "uuid": "^9.0.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/better-sqlite3": {
+ "version": "12.9.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
+ "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "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": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.6.6",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
+ "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "@types/ws": "^8.5.12",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.18.3"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.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/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
+ "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.8.3",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+ "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
+ "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.4.1",
+ "ws": "~8.18.3"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
+ "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..c0d99a7
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "watch-system-backend",
+ "version": "1.0.0",
+ "description": "Backend for Watch-System - AI Model Monitoring",
+ "main": "src/server.js",
+ "scripts": {
+ "start": "node src/server.js",
+ "dev": "nodemon src/server.js"
+ },
+ "dependencies": {
+ "better-sqlite3": "^12.9.0",
+ "cors": "^2.8.6",
+ "dotenv": "^16.6.1",
+ "express": "^4.22.1",
+ "socket.io": "^4.8.3",
+ "uuid": "^9.0.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+}
diff --git a/backend/src/database.js b/backend/src/database.js
new file mode 100644
index 0000000..a486a19
--- /dev/null
+++ b/backend/src/database.js
@@ -0,0 +1,206 @@
+'use strict';
+
+const Database = require('better-sqlite3');
+const path = require('path');
+const fs = require('fs');
+
+const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../data/watch.db');
+
+// Ensure data directory exists
+fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
+
+const db = new Database(DB_PATH);
+
+// Enable WAL mode for better concurrent read performance
+db.pragma('journal_mode = WAL');
+db.pragma('synchronous = NORMAL');
+
+// Create tables
+db.exec(`
+ CREATE TABLE IF NOT EXISTS models (
+ name TEXT PRIMARY KEY,
+ display_name TEXT NOT NULL,
+ description TEXT,
+ category TEXT
+ );
+
+ CREATE TABLE IF NOT EXISTS requests (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ model_name TEXT NOT NULL,
+ timestamp INTEGER NOT NULL,
+ response_time_ms INTEGER,
+ tokens_input INTEGER,
+ tokens_output INTEGER,
+ status TEXT NOT NULL DEFAULT 'success',
+ error_message TEXT,
+ FOREIGN KEY (model_name) REFERENCES models(name)
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_requests_model_ts ON requests(model_name, timestamp);
+ CREATE INDEX IF NOT EXISTS idx_requests_ts ON requests(timestamp);
+`);
+
+// Seed model definitions
+const seedModels = db.prepare(`
+ INSERT OR IGNORE INTO models (name, display_name, description, category)
+ VALUES (?, ?, ?, ?)
+`);
+
+const modelSeeds = [
+ ['llama2-7b', 'LLaMA2 7B', 'Meta 通用对话模型,7B 参数', 'chat'],
+ ['llama2-13b', 'LLaMA2 13B', 'Meta 通用对话模型,13B 参数', 'chat'],
+ ['mistral-7b', 'Mistral 7B', 'Mistral AI 快速推理模型', 'chat'],
+ ['codellama-7b', 'CodeLlama 7B', 'Meta 代码生成专用模型', 'code'],
+ ['phi-2', 'Phi-2', 'Microsoft 轻量高效小模型', 'chat'],
+ ['gemma-7b', 'Gemma 7B', 'Google DeepMind 开源模型', 'chat'],
+ ['qwen-7b', 'Qwen 7B', '阿里云通义千问多语言模型', 'multilingual'],
+ ['deepseek-7b', 'DeepSeek 7B', 'DeepSeek 深度推理模型', 'chat'],
+];
+
+const seedTx = db.transaction(() => {
+ for (const seed of modelSeeds) seedModels.run(...seed);
+});
+seedTx();
+
+// ─── Prepared Statements ─────────────────────────────────────────────────────
+
+const stmtInsertRequest = db.prepare(`
+ INSERT INTO requests (model_name, timestamp, response_time_ms, tokens_input, tokens_output, status, error_message)
+ VALUES (@model_name, @timestamp, @response_time_ms, @tokens_input, @tokens_output, @status, @error_message)
+`);
+
+const stmtGetAllModels = db.prepare(`SELECT * FROM models ORDER BY name`);
+
+const stmtGetModelStats = db.prepare(`
+ SELECT
+ model_name,
+ COUNT(*) AS total_requests,
+ AVG(response_time_ms) AS avg_response_time,
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS error_count,
+ SUM(tokens_input) AS total_tokens_in,
+ SUM(tokens_output) AS total_tokens_out
+ FROM requests
+ WHERE model_name = @model_name
+ AND timestamp >= @since
+`);
+
+const stmtGetRecentRequests = db.prepare(`
+ SELECT id, model_name, timestamp, response_time_ms, tokens_input, tokens_output, status, error_message
+ FROM requests
+ WHERE model_name = @model_name
+ ORDER BY timestamp DESC
+ LIMIT @limit
+`);
+
+const stmtGetGlobalStats = db.prepare(`
+ SELECT
+ COUNT(*) AS total_requests,
+ AVG(response_time_ms) AS avg_response_time,
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS error_count,
+ SUM(tokens_input) AS total_tokens_in,
+ SUM(tokens_output) AS total_tokens_out
+ FROM requests
+ WHERE timestamp >= @since
+`);
+
+const stmtGetPercentile = db.prepare(`
+ SELECT response_time_ms
+ FROM requests
+ WHERE model_name = @model_name
+ AND timestamp >= @since
+ AND status = 'success'
+ ORDER BY response_time_ms
+`);
+
+const stmtGetRequestsPerMinute = db.prepare(`
+ SELECT
+ (timestamp / 60000) * 60000 AS minute_bucket,
+ COUNT(*) AS count
+ FROM requests
+ WHERE model_name = @model_name
+ AND timestamp >= @since
+ GROUP BY minute_bucket
+ ORDER BY minute_bucket ASC
+`);
+
+const stmtGetAllModelStatsRange = db.prepare(`
+ SELECT
+ model_name,
+ COUNT(*) AS total_requests,
+ AVG(response_time_ms) AS avg_response_time,
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS error_count,
+ SUM(tokens_input) AS total_tokens_in,
+ SUM(tokens_output) AS total_tokens_out
+ FROM requests
+ WHERE timestamp >= @since
+ GROUP BY model_name
+`);
+
+// ─── Helper Functions ─────────────────────────────────────────────────────────
+
+function insertRequest(data) {
+ return stmtInsertRequest.run({
+ model_name: data.model_name,
+ timestamp: data.timestamp || Date.now(),
+ response_time_ms: data.response_time_ms,
+ tokens_input: data.tokens_input,
+ tokens_output: data.tokens_output,
+ status: data.status || 'success',
+ error_message: data.error_message || null,
+ });
+}
+
+function getAllModels() {
+ return stmtGetAllModels.all();
+}
+
+function getModelStats(modelName, windowMs = 60 * 1000) {
+ const since = Date.now() - windowMs;
+ return stmtGetModelStats.get({ model_name: modelName, since });
+}
+
+function getRecentRequests(modelName, limit = 100) {
+ return stmtGetRecentRequests.all({ model_name: modelName, limit });
+}
+
+function getAggregatedMetrics(windowMs = 60 * 1000) {
+ const since = Date.now() - windowMs;
+ const global = stmtGetGlobalStats.get({ since });
+ const perModel = stmtGetAllModelStatsRange.all({ since });
+ return { global, perModel };
+}
+
+function getPerModelDetailedStats(modelName, windowMs = 60 * 1000) {
+ const since = Date.now() - windowMs;
+ const stats = stmtGetModelStats.get({ model_name: modelName, since });
+ if (!stats || stats.total_requests === 0) return stats;
+
+ // Calculate P95 latency
+ const rows = stmtGetPercentile.all({ model_name: modelName, since });
+ let p95 = null;
+ if (rows.length > 0) {
+ const idx = Math.ceil(rows.length * 0.95) - 1;
+ p95 = rows[Math.min(idx, rows.length - 1)].response_time_ms;
+ }
+
+ const rpmRows = stmtGetRequestsPerMinute.all({ model_name: modelName, since: Date.now() - 10 * 60 * 1000 });
+
+ return {
+ ...stats,
+ p95_response_time: p95,
+ error_rate: stats.total_requests > 0 ? (stats.error_count / stats.total_requests) * 100 : 0,
+ requests_per_minute: stats.total_requests / (windowMs / 60000),
+ tokens_per_minute: (stats.total_tokens_in + stats.total_tokens_out) / (windowMs / 60000),
+ rpm_history: rpmRows,
+ };
+}
+
+module.exports = {
+ db,
+ insertRequest,
+ getAllModels,
+ getModelStats,
+ getRecentRequests,
+ getAggregatedMetrics,
+ getPerModelDetailedStats,
+};
diff --git a/backend/src/routes/api.js b/backend/src/routes/api.js
new file mode 100644
index 0000000..a6f94f7
--- /dev/null
+++ b/backend/src/routes/api.js
@@ -0,0 +1,143 @@
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+
+const {
+ getAllModels,
+ getModelStats,
+ getRecentRequests,
+ getAggregatedMetrics,
+ getPerModelDetailedStats,
+ insertRequest,
+} = require('../database');
+
+const alertManager = require('../services/alertManager');
+const simulator = require('../services/simulator');
+
+// GET /api/models
+router.get('/models', (req, res) => {
+ const models = getAllModels();
+ const profiles = simulator.getModelProfiles();
+ const windowMs = 60 * 1000;
+
+ const result = models.map(m => {
+ const stats = getModelStats(m.name, windowMs) || {};
+ const totalReq = stats.total_requests || 0;
+ const errCount = stats.error_count || 0;
+ const avgResp = stats.avg_response_time || 0;
+
+ const profile = profiles[m.name] || {};
+ let health = 'healthy';
+ if (totalReq > 0) {
+ const errorRate = errCount / totalReq;
+ if (errorRate > 0.1 || avgResp > 3000) health = 'degraded';
+ if (errorRate > 0.2) health = 'unhealthy';
+ }
+
+ return {
+ ...m,
+ emoji: profile.emoji || '🤖',
+ stats: {
+ total_requests: totalReq,
+ error_count: errCount,
+ avg_response_time: Math.round(avgResp),
+ error_rate: totalReq > 0 ? +((errCount / totalReq) * 100).toFixed(2) : 0,
+ requests_per_min: +(totalReq / (windowMs / 60000)).toFixed(2),
+ total_tokens_in: stats.total_tokens_in || 0,
+ total_tokens_out: stats.total_tokens_out || 0,
+ },
+ health,
+ };
+ });
+
+ res.json({ models: result, timestamp: Date.now() });
+});
+
+// GET /api/metrics
+router.get('/metrics', (req, res) => {
+ const windowMs = parseInt(req.query.window, 10) || 60 * 1000;
+ const { global: g, perModel } = getAggregatedMetrics(windowMs);
+
+ const totalReq = g.total_requests || 0;
+ const errCount = g.error_count || 0;
+
+ res.json({
+ total_requests: totalReq,
+ avg_response_time: g.avg_response_time ? Math.round(g.avg_response_time) : 0,
+ error_rate: totalReq > 0 ? +((errCount / totalReq) * 100).toFixed(2) : 0,
+ total_tokens_in: g.total_tokens_in || 0,
+ total_tokens_out: g.total_tokens_out || 0,
+ active_models: perModel.filter(m => m.total_requests > 0).length,
+ window_ms: windowMs,
+ timestamp: Date.now(),
+ });
+});
+
+// GET /api/metrics/:modelName
+router.get('/metrics/:modelName', (req, res) => {
+ const { modelName } = req.params;
+ const windowMs = parseInt(req.query.window, 10) || 60 * 1000;
+ const stats = getPerModelDetailedStats(modelName, windowMs);
+
+ if (!stats) {
+ return res.status(404).json({ error: `Model '${modelName}' not found` });
+ }
+
+ res.json({
+ model_name: modelName,
+ total_requests: stats.total_requests || 0,
+ avg_response_time: stats.avg_response_time ? Math.round(stats.avg_response_time) : 0,
+ p95_response_time: stats.p95_response_time || 0,
+ error_rate: +(stats.error_rate || 0).toFixed(2),
+ requests_per_min: +(stats.requests_per_minute || 0).toFixed(2),
+ tokens_per_min: +(stats.tokens_per_minute || 0).toFixed(2),
+ total_tokens_in: stats.total_tokens_in || 0,
+ total_tokens_out: stats.total_tokens_out || 0,
+ rpm_history: stats.rpm_history || [],
+ window_ms: windowMs,
+ timestamp: Date.now(),
+ });
+});
+
+// GET /api/history/:modelName
+router.get('/history/:modelName', (req, res) => {
+ const { modelName } = req.params;
+ const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500);
+ const rows = getRecentRequests(modelName, limit);
+ res.json({ model_name: modelName, requests: rows, count: rows.length });
+});
+
+// GET /api/alerts
+router.get('/alerts', (req, res) => {
+ res.json({ alerts: alertManager.getActiveAlerts(), timestamp: Date.now() });
+});
+
+// POST /api/simulate { model?: string }
+router.post('/simulate', (req, res) => {
+ const profiles = simulator.getModelProfiles();
+ const names = Object.keys(profiles);
+ const modelName = req.body.model && profiles[req.body.model]
+ ? req.body.model
+ : names[Math.floor(Math.random() * names.length)];
+
+ const profile = profiles[modelName];
+ const isError = Math.random() < profile.errorRate;
+
+ function randInt(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; }
+
+ const record = {
+ model_name: modelName,
+ timestamp: Date.now(),
+ response_time_ms: isError ? randInt(50, 300) : randInt(profile.minResponseMs, profile.maxResponseMs),
+ tokens_input: isError ? 0 : randInt(profile.minTokensIn, profile.maxTokensIn),
+ tokens_output: isError ? 0 : randInt(profile.minTokensOut, profile.maxTokensOut),
+ status: isError ? 'error' : 'success',
+ error_message: isError ? 'Manual simulation error' : null,
+ };
+
+ insertRequest(record);
+ res.json({ message: 'Simulation triggered', record });
+});
+
+module.exports = router;
diff --git a/backend/src/server.js b/backend/src/server.js
new file mode 100644
index 0000000..9984a91
--- /dev/null
+++ b/backend/src/server.js
@@ -0,0 +1,146 @@
+'use strict';
+
+require('dotenv').config();
+
+const express = require('express');
+const http = require('http');
+const { Server } = require('socket.io');
+const cors = require('cors');
+const path = require('path');
+
+const apiRouter = require('./routes/api');
+const simulator = require('./services/simulator');
+const alertManager = require('./services/alertManager');
+const { getAggregatedMetrics, getAllModels, getModelStats } = require('./database');
+
+const PORT = process.env.PORT || 3001;
+const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN || '*';
+
+// ─── App Setup ────────────────────────────────────────────────────────────────
+const app = express();
+const server = http.createServer(app);
+const io = new Server(server, {
+ cors: { origin: FRONTEND_ORIGIN, methods: ['GET', 'POST'] },
+});
+
+app.use(cors({ origin: FRONTEND_ORIGIN }));
+app.use(express.json());
+
+// Serve static frontend in production
+if (process.env.NODE_ENV === 'production') {
+ app.use(express.static(path.join(__dirname, '../../frontend')));
+}
+
+app.use('/api', apiRouter);
+
+// Health check
+app.get('/health', (_req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
+
+// ─── Socket.IO ────────────────────────────────────────────────────────────────
+io.on('connection', socket => {
+ console.log(`[Socket.IO] Client connected: ${socket.id}`);
+
+ // Send current state on connect
+ try {
+ const { global: g, perModel } = getAggregatedMetrics(60 * 1000);
+ const models = getAllModels();
+ socket.emit('init', { global: g, perModel, models });
+ } catch (err) {
+ console.error('[Socket.IO] Init error:', err.message);
+ }
+
+ socket.on('disconnect', () => {
+ console.log(`[Socket.IO] Client disconnected: ${socket.id}`);
+ });
+});
+
+// Forward simulator events to all connected clients
+simulator.on('request_completed', payload => {
+ io.emit('request_completed', payload);
+});
+
+// Broadcast full metrics every 5 seconds
+setInterval(() => {
+ try {
+ const windowMs = 60 * 1000;
+ const { global: g, perModel } = getAggregatedMetrics(windowMs);
+
+ const profiles = simulator.getModelProfiles();
+ const models = getAllModels();
+
+ const modelMetrics = models.map(m => {
+ const stats = perModel.find(p => p.model_name === m.name) || {};
+ const profile = profiles[m.name] || {};
+ const totalReq = stats.total_requests || 0;
+ const errCount = stats.error_count || 0;
+ const avgResp = stats.avg_response_time || 0;
+
+ let health = 'healthy';
+ if (totalReq > 0) {
+ const errorRate = errCount / totalReq;
+ if (errorRate > 0.1 || avgResp > 3000) health = 'degraded';
+ if (errorRate > 0.2) health = 'unhealthy';
+ }
+
+ return {
+ name: m.name,
+ displayName: m.display_name,
+ emoji: profile.emoji || '🤖',
+ category: m.category,
+ health,
+ stats: {
+ total_requests: totalReq,
+ error_count: errCount,
+ avg_response_time: Math.round(avgResp),
+ error_rate: totalReq > 0 ? +((errCount / totalReq) * 100).toFixed(2) : 0,
+ requests_per_min: +(totalReq / (windowMs / 60000)).toFixed(2),
+ total_tokens_in: stats.total_tokens_in || 0,
+ total_tokens_out: stats.total_tokens_out || 0,
+ },
+ };
+ });
+
+ const totalReq = g.total_requests || 0;
+ const errCount = g.error_count || 0;
+
+ io.emit('metrics_update', {
+ global: {
+ total_requests: totalReq,
+ avg_response_time: g.avg_response_time ? Math.round(g.avg_response_time) : 0,
+ error_rate: totalReq > 0 ? +((errCount / totalReq) * 100).toFixed(2) : 0,
+ total_tokens_in: g.total_tokens_in || 0,
+ total_tokens_out: g.total_tokens_out || 0,
+ active_models: modelMetrics.filter(m => m.stats.total_requests > 0).length,
+ },
+ models: modelMetrics,
+ timestamp: Date.now(),
+ });
+ } catch (err) {
+ console.error('[Metrics] Broadcast error:', err.message);
+ }
+}, 5000);
+
+// Forward alert events
+alertManager.on('alert_created', alert => io.emit('alert_created', alert));
+alertManager.on('alert_resolved', alert => io.emit('alert_resolved', alert));
+
+// ─── Start ────────────────────────────────────────────────────────────────────
+server.listen(PORT, () => {
+ console.log(`[Server] Watch-System backend running on http://localhost:${PORT}`);
+ console.log(`[Server] Environment: ${process.env.NODE_ENV || 'development'}`);
+
+ simulator.start();
+ alertManager.start();
+});
+
+// Graceful shutdown
+process.on('SIGTERM', () => {
+ simulator.stop();
+ alertManager.stop();
+ server.close(() => {
+ console.log('[Server] Gracefully shut down');
+ process.exit(0);
+ });
+});
+
+module.exports = { app, server, io };
diff --git a/backend/src/services/alertManager.js b/backend/src/services/alertManager.js
new file mode 100644
index 0000000..99cd5c9
--- /dev/null
+++ b/backend/src/services/alertManager.js
@@ -0,0 +1,136 @@
+'use strict';
+
+const EventEmitter = require('events');
+const { getAggregatedMetrics, getModelStats } = require('../database');
+
+const CHECK_INTERVAL_MS = 10000; // Check every 10 seconds
+const WINDOW_MS = 60 * 1000; // 1-minute rolling window
+
+const THRESHOLDS = {
+ ERROR_RATE_PCT: 10, // > 10% errors → alert
+ SLOW_RESPONSE_MS: 3000, // > 3000ms avg → alert
+ SPIKE_MULTIPLIER: 2.5, // > 2.5× baseline requests → alert
+};
+
+class AlertManager extends EventEmitter {
+ constructor() {
+ super();
+ this._activeAlerts = new Map(); // key: `${modelName}:${type}`
+ this._timer = null;
+ this._running = false;
+ this._baselineRpm = new Map(); // model → baseline requests/min
+ }
+
+ _alertKey(modelName, type) {
+ return `${modelName}:${type}`;
+ }
+
+ _createAlert(modelName, type, severity, message, value) {
+ const key = this._alertKey(modelName, type);
+ if (this._activeAlerts.has(key)) return; // deduplicate
+
+ const alert = {
+ id: `${key}:${Date.now()}`,
+ modelName,
+ type,
+ severity,
+ message,
+ value,
+ createdAt: Date.now(),
+ };
+
+ this._activeAlerts.set(key, alert);
+ console.log(`[AlertManager] ALERT CREATED: ${message}`);
+ this.emit('alert_created', alert);
+ }
+
+ _resolveAlert(modelName, type) {
+ const key = this._alertKey(modelName, type);
+ if (!this._activeAlerts.has(key)) return;
+
+ const alert = this._activeAlerts.get(key);
+ this._activeAlerts.delete(key);
+ console.log(`[AlertManager] ALERT RESOLVED: ${alert.message}`);
+ this.emit('alert_resolved', { ...alert, resolvedAt: Date.now() });
+ }
+
+ _checkModel(modelName, stats) {
+ if (!stats || stats.total_requests === 0) return;
+
+ const errorRate = (stats.error_count / stats.total_requests) * 100;
+ const avgResponse = stats.avg_response_time || 0;
+ const rpm = stats.total_requests / (WINDOW_MS / 60000);
+
+ // ── Error rate check ──────────────────────────────────────
+ if (errorRate > THRESHOLDS.ERROR_RATE_PCT) {
+ this._createAlert(
+ modelName, 'high_error_rate', 'error',
+ `${modelName} 错误率过高: ${errorRate.toFixed(1)}%`,
+ errorRate
+ );
+ } else {
+ this._resolveAlert(modelName, 'high_error_rate');
+ }
+
+ // ── Slow response check ───────────────────────────────────
+ if (avgResponse > THRESHOLDS.SLOW_RESPONSE_MS) {
+ this._createAlert(
+ modelName, 'slow_response', 'warning',
+ `${modelName} 响应时间过慢: ${Math.round(avgResponse)}ms`,
+ avgResponse
+ );
+ } else {
+ this._resolveAlert(modelName, 'slow_response');
+ }
+
+ // ── Traffic spike check ───────────────────────────────────
+ const baseline = this._baselineRpm.get(modelName);
+ if (baseline && rpm > baseline * THRESHOLDS.SPIKE_MULTIPLIER) {
+ this._createAlert(
+ modelName, 'traffic_spike', 'warning',
+ `${modelName} 流量突增: ${rpm.toFixed(1)} req/min (基线: ${baseline.toFixed(1)})`,
+ rpm
+ );
+ } else {
+ this._resolveAlert(modelName, 'traffic_spike');
+ }
+
+ // Update rolling baseline (exponential moving average)
+ const alpha = 0.1;
+ const prev = this._baselineRpm.get(modelName) || rpm;
+ this._baselineRpm.set(modelName, alpha * rpm + (1 - alpha) * prev);
+ }
+
+ async _check() {
+ try {
+ const { perModel } = getAggregatedMetrics(WINDOW_MS);
+ for (const stats of perModel) {
+ this._checkModel(stats.model_name, stats);
+ }
+ } catch (err) {
+ console.error('[AlertManager] Check error:', err.message);
+ }
+ }
+
+ start() {
+ if (this._running) return;
+ this._running = true;
+ this._timer = setInterval(() => this._check(), CHECK_INTERVAL_MS);
+ console.log('[AlertManager] Started');
+ }
+
+ stop() {
+ this._running = false;
+ if (this._timer) {
+ clearInterval(this._timer);
+ this._timer = null;
+ }
+ console.log('[AlertManager] Stopped');
+ }
+
+ getActiveAlerts() {
+ return Array.from(this._activeAlerts.values());
+ }
+}
+
+module.exports = new AlertManager();
diff --git a/backend/src/services/simulator.js b/backend/src/services/simulator.js
new file mode 100644
index 0000000..1164a99
--- /dev/null
+++ b/backend/src/services/simulator.js
@@ -0,0 +1,171 @@
+'use strict';
+
+const EventEmitter = require('events');
+const { insertRequest } = require('../database');
+
+// Model personality profiles
+const MODEL_PROFILES = {
+ 'llama2-7b': {
+ displayName: 'LLaMA2 7B',
+ minResponseMs: 800, maxResponseMs: 2000,
+ minTokensIn: 50, maxTokensIn: 512,
+ minTokensOut: 100, maxTokensOut: 800,
+ errorRate: 0.03,
+ emoji: '🦙',
+ },
+ 'llama2-13b': {
+ displayName: 'LLaMA2 13B',
+ minResponseMs: 1500, maxResponseMs: 4000,
+ minTokensIn: 80, maxTokensIn: 600,
+ minTokensOut: 150, maxTokensOut: 1200,
+ errorRate: 0.04,
+ emoji: '🦙',
+ },
+ 'mistral-7b': {
+ displayName: 'Mistral 7B',
+ minResponseMs: 400, maxResponseMs: 1200,
+ minTokensIn: 60, maxTokensIn: 450,
+ minTokensOut: 80, maxTokensOut: 700,
+ errorRate: 0.02,
+ emoji: '🌪️',
+ },
+ 'codellama-7b': {
+ displayName: 'CodeLlama 7B',
+ minResponseMs: 600, maxResponseMs: 1800,
+ minTokensIn: 100, maxTokensIn: 800,
+ minTokensOut: 200, maxTokensOut: 1500,
+ errorRate: 0.05,
+ emoji: '💻',
+ },
+ 'phi-2': {
+ displayName: 'Phi-2',
+ minResponseMs: 300, maxResponseMs: 900,
+ minTokensIn: 30, maxTokensIn: 300,
+ minTokensOut: 50, maxTokensOut: 500,
+ errorRate: 0.02,
+ emoji: '⚡',
+ },
+ 'gemma-7b': {
+ displayName: 'Gemma 7B',
+ minResponseMs: 500, maxResponseMs: 1500,
+ minTokensIn: 60, maxTokensIn: 480,
+ minTokensOut: 90, maxTokensOut: 750,
+ errorRate: 0.03,
+ emoji: '💎',
+ },
+ 'qwen-7b': {
+ displayName: 'Qwen 7B',
+ minResponseMs: 400, maxResponseMs: 1100,
+ minTokensIn: 70, maxTokensIn: 500,
+ minTokensOut: 100, maxTokensOut: 800,
+ errorRate: 0.03,
+ emoji: '🌏',
+ },
+ 'deepseek-7b': {
+ displayName: 'DeepSeek 7B',
+ minResponseMs: 600, maxResponseMs: 1600,
+ minTokensIn: 80, maxTokensIn: 550,
+ minTokensOut: 120, maxTokensOut: 900,
+ errorRate: 0.04,
+ emoji: '🔍',
+ },
+};
+
+const MODEL_NAMES = Object.keys(MODEL_PROFILES);
+
+function randInt(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+function randFloat(min, max) {
+ return Math.random() * (max - min) + min;
+}
+
+class Simulator extends EventEmitter {
+ constructor() {
+ super();
+ this._timer = null;
+ this._running = false;
+ }
+
+ _simulateOne() {
+ const modelName = MODEL_NAMES[Math.floor(Math.random() * MODEL_NAMES.length)];
+ const profile = MODEL_PROFILES[modelName];
+
+ const isError = Math.random() < profile.errorRate;
+ const responseTime = isError
+ ? randInt(50, 300)
+ : randInt(profile.minResponseMs, profile.maxResponseMs);
+
+ const tokensIn = isError ? 0 : randInt(profile.minTokensIn, profile.maxTokensIn);
+ const tokensOut = isError ? 0 : randInt(profile.minTokensOut, profile.maxTokensOut);
+
+ const errorMessages = [
+ 'Rate limit exceeded',
+ 'Context length exceeded',
+ 'Out of memory',
+ 'Model loading timeout',
+ 'Invalid request format',
+ ];
+
+ const record = {
+ model_name: modelName,
+ timestamp: Date.now(),
+ response_time_ms: responseTime,
+ tokens_input: tokensIn,
+ tokens_output: tokensOut,
+ status: isError ? 'error' : 'success',
+ error_message: isError ? errorMessages[Math.floor(Math.random() * errorMessages.length)] : null,
+ };
+
+ try {
+ insertRequest(record);
+ } catch (err) {
+ // Non-fatal: log and continue
+ console.error('[Simulator] DB insert error:', err.message);
+ }
+
+ this.emit('request_completed', {
+ model: modelName,
+ displayName: profile.displayName,
+ emoji: profile.emoji,
+ responseTime,
+ tokensIn,
+ tokensOut,
+ status: record.status,
+ errorMessage: record.error_message,
+ timestamp: record.timestamp,
+ });
+ }
+
+ _scheduleNext() {
+ if (!this._running) return;
+ const delay = randInt(200, 800);
+ this._timer = setTimeout(() => {
+ this._simulateOne();
+ this._scheduleNext();
+ }, delay);
+ }
+
+ start() {
+ if (this._running) return;
+ this._running = true;
+ console.log('[Simulator] Started');
+ this._scheduleNext();
+ }
+
+ stop() {
+ this._running = false;
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ }
+ console.log('[Simulator] Stopped');
+ }
+
+ getModelProfiles() {
+ return MODEL_PROFILES;
+ }
+}
+
+module.exports = new Simulator();
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..bf56bec
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,19 @@
+version: '3.8'
+services:
+ backend:
+ build: ./backend
+ ports:
+ - "3001:3001"
+ environment:
+ - NODE_ENV=production
+ - PORT=3001
+ volumes:
+ - ./data:/app/data
+ frontend:
+ image: nginx:alpine
+ ports:
+ - "3000:80"
+ volumes:
+ - ./frontend:/usr/share/nginx/html
+ depends_on:
+ - backend
diff --git a/docs/project-plan.md b/docs/project-plan.md
new file mode 100644
index 0000000..274428c
--- /dev/null
+++ b/docs/project-plan.md
@@ -0,0 +1,221 @@
+# Watch-System 项目规划文档
+
+## 一、项目背景
+
+随着开源大语言模型(LLM)的爆发式增长,企业和开发者在生产环境中部署了越来越多的 AI 模型服务。如何实时掌握这些模型的运行状态、性能指标以及异常情况,成为 MLOps 工程师面临的核心挑战之一。
+
+Watch-System 旨在提供一套轻量、可扩展的监控解决方案,针对开源模型(LLaMA2、Mistral、CodeLlama 等)的 API 调用进行全方位监控,帮助团队:
+
+- 快速发现性能瓶颈(响应时间过长、错误率上升)
+- 掌握各模型的资源消耗(Token 使用量)
+- 在异常发生时及时告警,缩短 MTTR(平均故障恢复时间)
+
+---
+
+## 二、系统架构
+
+### 2.1 整体架构
+
+系统采用前后端分离架构,后端为 Node.js 服务,前端为单页应用(SPA)。
+
+```
+前端 (SPA) 后端 (Node.js) 数据层
+ │ │ │
+ │── HTTP GET ──────► REST API │
+ │ │── 查询 ──────────► SQLite DB
+ │◄─ WebSocket ────── Socket.IO │
+ │ (实时推送) │── 写入 ──────────► SQLite DB
+ │
+ 模拟器 / 告警管理器
+```
+
+### 2.2 技术选型理由
+
+| 技术 | 版本 | 选型理由 |
+|------|------|----------|
+| Node.js + Express | 18.x / 4.x | 轻量、生态丰富、异步友好 |
+| Socket.IO | 4.x | 自动降级、断线重连、房间广播 |
+| better-sqlite3 | 9.x | 同步 API、高性能、零配置 |
+| Chart.js | 4.x | 轻量图表库,支持实时更新 |
+| Docker Compose | 3.8 | 一键编排,环境一致性 |
+
+---
+
+## 三、模块拆解
+
+### 3.1 后端模块
+
+#### server.js(主入口)
+- 初始化 Express 应用
+- 挂载 CORS、JSON 中间件
+- 注册 `/api` 路由
+- 启动 Socket.IO 服务
+- 启动模拟器和告警管理器
+- 监听指定端口
+
+#### database.js(数据层)
+- 初始化 SQLite 数据库
+- 创建 `requests` 表和 `models` 表
+- 提供以下 Helper 函数:
+ - `insertRequest()` - 写入请求记录
+ - `getModelStats()` - 查询模型统计数据
+ - `getRecentRequests()` - 查询近期请求列表
+ - `getAggregatedMetrics()` - 聚合全局指标
+
+#### routes/api.js(REST API)
+- `GET /api/models` - 返回所有模型列表及实时状态
+- `GET /api/metrics` - 全局聚合指标
+- `GET /api/metrics/:modelName` - 单模型指标(含 P95 延迟)
+- `GET /api/history/:modelName` - 最近 100 条请求历史
+- `GET /api/alerts` - 活跃告警列表
+- `POST /api/simulate` - 手动触发单次模拟
+
+#### services/simulator.js(模拟器)
+- 定义 8 个模型的特征参数(响应时间范围、Token 范围、错误率)
+- 随机化请求间隔(200-800ms)
+- 每次模拟完成后:
+ 1. 写入数据库
+ 2. 触发 `request_completed` 事件
+- 支持启动/停止控制
+
+#### services/alertManager.js(告警管理)
+- 定期检查指标阈值:
+ - 错误率 > 10%:ERROR 级别告警
+ - 平均响应时间 > 3000ms:WARNING 级别告警
+ - 请求量突增 > 200%:WARNING 级别告警
+- 告警去重(同一模型同一类型不重复创建)
+- 指标恢复后自动解除告警
+- 触发 `alert_created` / `alert_resolved` 事件
+
+### 3.2 前端模块
+
+#### 布局结构
+```
+┌──────────────── Header ───────────────────┐
+│ AI 模型监控系统 [实时时钟] [状态] │
+├──────── Stats Cards (4列) ────────────────┤
+│ 总请求数 │ 活跃模型 │ 均响应时间 │ 错误率 │
+├────── Charts (左2/3 + 右1/3) ─────────────┤
+│ 请求量趋势折线图 │ 请求分布饼图 │
+├────── 响应时间柱状图 ──────────────────────┤
+│ 各模型平均响应时间横向柱状图 │
+├──── Model Cards Grid (4x2) ───────────────┤
+│ [模型卡片×8] │
+├──── Live Log + Alerts ────────────────────┤
+│ 实时请求日志表格 │ 告警面板 │
+└───────────────────────────────────────────┘
+```
+
+---
+
+## 四、数据流设计
+
+### 4.1 实时数据流
+
+```
+Simulator.js
+ │
+ │ 每 200-800ms 生成一条模拟请求
+ ▼
+database.insertRequest()
+ │
+ │ 写入 SQLite
+ ▼
+EventEmitter.emit('request_completed', payload)
+ │
+ ▼
+server.js 监听事件
+ │
+ │ io.emit('request_completed', payload)
+ ▼
+所有连接的前端客户端
+ │
+ ▼
+更新实时日志 + 更新模型卡片计数器
+```
+
+### 4.2 定时指标推送流
+
+```
+server.js setInterval(5000)
+ │
+ │ 调用 database.getAggregatedMetrics()
+ ▼
+io.emit('metrics_update', allMetrics)
+ │
+ ▼
+前端更新:
+ - Stats Cards 数字动画
+ - 折线图新增数据点
+ - 饼图重绘
+ - 柱状图重绘
+ - 模型卡片状态更新
+```
+
+### 4.3 告警流
+
+```
+alertManager.js setInterval(10000)
+ │
+ │ 查询最近 1 分钟各模型指标
+ ▼
+检查阈值条件
+ │
+ ├── 新告警 → emit('alert_created')
+ │ → 前端显示告警横幅
+ │
+ └── 告警恢复 → emit('alert_resolved')
+ → 前端标记告警已解除
+```
+
+---
+
+## 五、数据库设计
+
+### requests 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | INTEGER PRIMARY KEY | 自增主键 |
+| model_name | TEXT NOT NULL | 模型标识符 |
+| timestamp | INTEGER NOT NULL | Unix 时间戳(毫秒) |
+| response_time_ms | INTEGER | 响应时间(毫秒) |
+| tokens_input | INTEGER | 输入 Token 数 |
+| tokens_output | INTEGER | 输出 Token 数 |
+| status | TEXT | success / error |
+| error_message | TEXT | 错误信息(可空) |
+
+### models 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| name | TEXT PRIMARY KEY | 模型唯一标识 |
+| display_name | TEXT | 展示名称 |
+| description | TEXT | 模型描述 |
+| category | TEXT | 分类(chat/code/multilingual) |
+
+---
+
+## 六、开发里程碑
+
+| 阶段 | 任务 | 状态 |
+|------|------|------|
+| M1 | 项目初始化、目录结构、package.json | ✅ 完成 |
+| M2 | 数据库设计与实现(database.js) | ✅ 完成 |
+| M3 | 模型模拟器实现(simulator.js) | ✅ 完成 |
+| M4 | REST API 路由实现(api.js) | ✅ 完成 |
+| M5 | 告警管理器(alertManager.js) | ✅ 完成 |
+| M6 | 主服务入口(server.js) | ✅ 完成 |
+| M7 | 前端仪表板(index.html) | ✅ 完成 |
+| M8 | Docker 化部署配置 | ✅ 完成 |
+| M9 | 端到端联调测试 | ✅ 完成 |
+
+---
+
+## 七、扩展规划
+
+- **多实例支持**:引入 Redis Pub/Sub 替换内存事件总线,支持水平扩展
+- **真实接入**:对接 Ollama / vLLM 等本地推理框架的真实 API
+- **用户认证**:添加 JWT 鉴权,支持多租户监控
+- **数据导出**:支持 Prometheus 指标格式导出,对接 Grafana
+- **移动端适配**:PWA 支持,手机端查看告警推送
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..0f89314
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,841 @@
+
+
+
+
+
+ AI 模型监控系统
+
+
+
+
+
+
+
+
+
+
+
+
+ 📊 核心指标(最近1分钟)
+
+
+
+
🤖
+
活跃模型
+
0
+
/ 8 个模型
+
+
+
⏱️
+
平均响应时间
+
0 ms
+
—
+
+
+
+
+
+ 📈 实时图表
+
+
+
+
+
+
⏳ 各模型平均响应时间 (ms)
+
+
+
+
+
+
+
+ 🤖 模型状态
+
+
+
+
+
+ 📋 实时日志 & 告警
+
+
+
📝 请求日志 0
+
+
+
+
+ | 时间 |
+ 模型 |
+ 状态 |
+ 耗时 |
+ 输入 Token |
+ 输出 Token |
+
+
+
+ | 等待数据… |
+
+
+
+
+
+
+
+
+
+
+
+
+
From ef545385dd62d99315748156fa1137c79ee4c8c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 07:49:57 +0000
Subject: [PATCH 3/3] Fix CORS security, local JS libs, and code review
improvements
Agent-Logs-Url: https://github.com/LLibra1/Watch-System/sessions/28e34e43-2d8e-4fc8-961b-126c83bb63d2
Co-authored-by: LLibra1 <195303685+LLibra1@users.noreply.github.com>
---
.gitignore | 3 +++
README.md | 21 +++++++++++++--------
backend/src/server.js | 21 +++++++++++++++------
backend/src/services/simulator.js | 7 ++++++-
frontend/index.html | 11 ++++++++---
frontend/lib/chart.umd.min.js | 14 ++++++++++++++
frontend/lib/socket.io.min.js | 7 +++++++
7 files changed, 66 insertions(+), 18 deletions(-)
create mode 100644 frontend/lib/chart.umd.min.js
create mode 100644 frontend/lib/socket.io.min.js
diff --git a/.gitignore b/.gitignore
index e4d8733..0d7f3f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ data/
*.db-shm
*.db-wal
.env
+node_modules/
+npm-debug.log*
+.DS_Store
diff --git a/README.md b/README.md
index 4cd4799..23de5a4 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
> 实时监控开源 AI/ML 模型调用状态的可视化仪表板
+
+
---
## 📋 项目概述
@@ -69,13 +71,12 @@ Watch-System 是一套针对开源 AI/ML 模型(如 LLaMA2、Mistral、CodeLla
cd backend
npm install
-# 2. 启动后端服务
-npm start
-# 或开发模式(热重载)
-npm run dev
+# 2. 生产模式启动(同时提供前端服务)
+NODE_ENV=production npm start
+# 浏览器访问: http://localhost:3001
-# 3. 打开前端
-open ../frontend/index.html
+# 或开发模式(仅 API,前端直接打开 frontend/index.html)
+npm run dev
```
### 方式二:Docker Compose
@@ -91,8 +92,9 @@ docker-compose up --build
| 变量 | 默认值 | 说明 |
|------|--------|------|
| PORT | 3001 | 后端监听端口 |
-| NODE_ENV | development | 运行环境 |
+| NODE_ENV | development | 运行环境(production 时同时提供前端静态文件) |
| DB_PATH | ./data/watch.db | SQLite 数据库路径 |
+| FRONTEND_ORIGIN | localhost:3000,3001 | 开发模式允许的 CORS 来源(逗号分隔) |
---
@@ -105,7 +107,10 @@ Watch-System/
├── docs/
│ └── project-plan.md
├── frontend/
-│ └── index.html
+│ ├── index.html
+│ └── lib/ # 本地 JS 库(无需 CDN)
+│ ├── chart.umd.min.js
+│ └── socket.io.min.js
└── backend/
├── Dockerfile
├── package.json
diff --git a/backend/src/server.js b/backend/src/server.js
index 9984a91..e5cdb0f 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -14,20 +14,29 @@ const alertManager = require('./services/alertManager');
const { getAggregatedMetrics, getAllModels, getModelStats } = require('./database');
const PORT = process.env.PORT || 3001;
-const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN || '*';
+const isProduction = process.env.NODE_ENV === 'production';
+
+// In production the frontend is served from the same origin, so CORS is not needed.
+// In development, restrict to localhost only (no wildcard).
+const DEV_ORIGINS = process.env.FRONTEND_ORIGIN
+ ? process.env.FRONTEND_ORIGIN.split(',').map(s => s.trim())
+ : [`http://localhost:3000`, `http://localhost:${PORT}`, `http://127.0.0.1:${PORT}`];
+
+const corsOptions = {
+ origin: isProduction ? false : DEV_ORIGINS,
+ methods: ['GET', 'POST'],
+};
// ─── App Setup ────────────────────────────────────────────────────────────────
const app = express();
const server = http.createServer(app);
-const io = new Server(server, {
- cors: { origin: FRONTEND_ORIGIN, methods: ['GET', 'POST'] },
-});
+const io = new Server(server, { cors: corsOptions });
-app.use(cors({ origin: FRONTEND_ORIGIN }));
+app.use(cors(corsOptions));
app.use(express.json());
// Serve static frontend in production
-if (process.env.NODE_ENV === 'production') {
+if (isProduction) {
app.use(express.static(path.join(__dirname, '../../frontend')));
}
diff --git a/backend/src/services/simulator.js b/backend/src/services/simulator.js
index 1164a99..eb7282d 100644
--- a/backend/src/services/simulator.js
+++ b/backend/src/services/simulator.js
@@ -3,6 +3,11 @@
const EventEmitter = require('events');
const { insertRequest } = require('../database');
+// Interval range (ms) between simulated requests.
+// Keeps the simulator from running too fast or too slow for demo purposes.
+const MIN_REQUEST_DELAY_MS = 200;
+const MAX_REQUEST_DELAY_MS = 800;
+
// Model personality profiles
const MODEL_PROFILES = {
'llama2-7b': {
@@ -140,7 +145,7 @@ class Simulator extends EventEmitter {
_scheduleNext() {
if (!this._running) return;
- const delay = randInt(200, 800);
+ const delay = randInt(MIN_REQUEST_DELAY_MS, MAX_REQUEST_DELAY_MS);
this._timer = setTimeout(() => {
this._simulateOne();
this._scheduleNext();
diff --git a/frontend/index.html b/frontend/index.html
index 0f89314..da5d2df 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,8 +4,8 @@
AI 模型监控系统
-
-
+
+