🔄 请求量趋势(每分钟)
+🥧 请求分布
+📝 请求日志 0
+| 时间 | +模型 | +状态 | +耗时 | +输入 Token | +输出 Token | +
|---|---|---|---|---|---|
| 等待数据… | |||||
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d7f3f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +data/ +*.db +*.db-shm +*.db-wal +.env +node_modules/ +npm-debug.log* +.DS_Store diff --git a/README.md b/README.md index d82f603..23de5a4 100644 --- a/README.md +++ b/README.md @@ -1 +1,170 @@ -# 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. 生产模式启动(同时提供前端服务) +NODE_ENV=production npm start +# 浏览器访问: http://localhost:3001 + +# 或开发模式(仅 API,前端直接打开 frontend/index.html) +npm run dev +``` + +### 方式二:Docker Compose + +```bash +docker-compose up --build +# 前端访问: http://localhost:3000 +# 后端 API: http://localhost:3001 +``` + +### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| PORT | 3001 | 后端监听端口 | +| NODE_ENV | development | 运行环境(production 时同时提供前端静态文件) | +| DB_PATH | ./data/watch.db | SQLite 数据库路径 | +| FRONTEND_ORIGIN | localhost:3000,3001 | 开发模式允许的 CORS 来源(逗号分隔) | + +--- + +## 📁 项目结构 + +``` +Watch-System/ +├── README.md +├── docker-compose.yml +├── docs/ +│ └── project-plan.md +├── frontend/ +│ ├── index.html +│ └── lib/ # 本地 JS 库(无需 CDN) +│ ├── chart.umd.min.js +│ └── socket.io.min.js +└── 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..e5cdb0f --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,155 @@ +'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 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: corsOptions }); + +app.use(cors(corsOptions)); +app.use(express.json()); + +// Serve static frontend in production +if (isProduction) { + 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..eb7282d --- /dev/null +++ b/backend/src/services/simulator.js @@ -0,0 +1,176 @@ +'use strict'; + +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': { + 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(MIN_REQUEST_DELAY_MS, MAX_REQUEST_DELAY_MS); + 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..da5d2df --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,846 @@ + + +
+ + +| 时间 | +模型 | +状态 | +耗时 | +输入 Token | +输出 Token | +
|---|---|---|---|---|---|
| 等待数据… | |||||