commit aa4b14181c2612054ac6c97d1278ba95085a32b6 Author: root <13910913995@163.com> Date: Mon Jan 19 14:27:41 2026 +0800 chore: 初始化项目并添加 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..980232a --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# ===== OS / Editor ===== +.DS_Store +Thumbs.db +Desktop.ini +*.swp +*.swo +*.tmp +*.bak + +.idea/ +.vscode/ + +# ===== Logs ===== +*.log + +# ===== Python ===== +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +coverage.xml +htmlcov/ +*.egg-info/ +dist/ +build/ + +.venv/ +venv/ +env/ +ENV/ + +.env +.env.* +!.env.example + +# ===== Backend (PyInstaller outputs) ===== +/backend/build/ +/backend/dist/ +/backend/venv/ + +# ===== Node / Frontend ===== +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +/.turbo/ +/.cache/ + +/frontend/node_modules/ +/frontend/dist/ +/frontend/release/ + +# ===== Electron ===== +*.asar + diff --git a/.trae/skills/code-reviewer/SKILL.md b/.trae/skills/code-reviewer/SKILL.md new file mode 100644 index 0000000..3d76b23 --- /dev/null +++ b/.trae/skills/code-reviewer/SKILL.md @@ -0,0 +1,61 @@ +--- +name: "code-reviewer" +description: "审查代码的正确性、安全性、可维护性与一致性。用户要求“代码审查/Review”、准备合并、或你完成较大改动后调用。" +--- + +# Code Reviewer(代码审查) + +## 何时使用 + +- 用户明确提出“代码审查/Review/帮我看看这段代码/有没有坑” +- 你完成了较大改动(多文件、涉及业务逻辑/安全/性能/并发/数据库/鉴权),准备交付前自检 +- 修复 bug 后,需要确认没有引入回归 + +## 输入要求(你需要我提供什么) + +优先按可获得的信息审查,不强制全部具备: + +- 需要审查的范围:文件路径/目录/提交 diff/粘贴代码片段 +- 目标与约束:功能期望、性能目标、兼容性、上线环境、风格偏好 +- 相关上下文:调用链入口、接口契约(请求/响应)、数据库表/索引、配置项 + +## 审查维度(检查清单) + +### 正确性 +- 边界条件、空值、异常路径、重试/幂等、并发与竞态 +- 输入校验与错误信息一致性 +- 资源释放(文件句柄、连接、锁)、超时设置 + +### 安全性 +- 注入风险(SQL/命令/模板/路径穿越)、反序列化风险 +- 鉴权/鉴别:敏感操作是否校验权限,是否存在越权路径 +- 机密信息:日志/错误信息/返回值中是否泄露 token、密码、密钥、PII + +### 可维护性 +- 代码结构是否清晰,职责是否单一,命名是否表达意图 +- 重复逻辑是否可抽取,是否遵循既有项目模式 +- 配置/常量是否集中管理,错误码/异常是否统一 + +### 性能与稳定性 +- 热路径复杂度、N+1、无界循环/递归、潜在阻塞 I/O +- 缓存策略、批处理、分页、索引友好性 +- 观测性:日志粒度、指标点、错误上下文是否足够定位 + +### 风格与一致性 +- 与仓库既有格式、依赖、工具链一致(类型、lint、格式化、文件组织) +- 公共工具与约定(例如 logger、配置、错误处理)是否复用而非自造 + +## 输出格式(你应如何给出审查结果) + +按“可执行、可落地”的方式输出,默认用中文: + +1. 结论摘要(风险等级:高/中/低) +2. 必须修复(Blockers):每条包含 位置、问题、影响、建议修复 +3. 建议改进(Suggestions):同上,但不阻塞 +4. 可选优化(Nice-to-have) +5. 若适用:给出最小改动的补丁方案(优先)或替代实现思路 + +## 约束 + +- 不凭空假设依赖已存在;若引入新依赖,先确认仓库已有或给出替代方案 +- 不输出或建议写入任何密钥/token diff --git a/.trae/skills/pr-reviewer/SKILL.md b/.trae/skills/pr-reviewer/SKILL.md new file mode 100644 index 0000000..e295f13 --- /dev/null +++ b/.trae/skills/pr-reviewer/SKILL.md @@ -0,0 +1,86 @@ +--- +name: "pr-reviewer" +description: "对变更/PR 做代码审查并给出可执行修改建议。用户要求代码评审、准备合并、或完成较大改动后自检时调用。" +--- + +# PR Reviewer(变更/PR 代码审查) + +## 目标 + +- 快速识别高风险缺陷(正确性/安全/稳定性/兼容性) +- 以“可落地修改”为导向给出建议(位置、原因、影响、最小修复) +- 在不引入不必要依赖的前提下,尽量复用仓库现有模式与工具 + +## 何时使用 + +- 用户要求“评审这个 PR/diff/改动” +- 你完成了多文件或关键路径改动,准备合并前自检 +- 修复 bug 或安全问题后,需要确认无回归 + +## 你需要获取的信息(尽量自己补齐) + +优先使用现有上下文完成评审,不强制用户补全: + +- 变更范围:diff/文件列表/目录 +- 预期行为:功能目标、边界条件、失败语义 +- 运行环境:语言版本、部署方式、配置来源、数据库/外部依赖 +- 质量门槛:性能目标、SLA、兼容性、测试要求 + +## 审查方法(按优先级) + +1. 先看变更入口与数据流:输入→处理→持久化/外部调用→输出 +2. 先找会导致事故的点:鉴权、注入、并发、资源泄露、回滚困难 +3. 再看可维护性:重复、耦合、命名、可读性、错误处理一致性 +4. 最后看体验与一致性:日志、可观测性、风格、文档与配置 + +## 检查清单 + +### 正确性 + +- 边界条件:空值/缺省、极端长度、时区与编码、浮点与精度 +- 错误路径:异常是否被吞、错误码与消息是否一致、重试/幂等性 +- 并发:竞态、锁粒度、事务边界、重复提交 +- 资源:连接/句柄/任务是否可控,是否有超时与取消 + +### 安全性 + +- 注入:SQL/命令/模板/路径穿越/正则 DoS +- 鉴权:敏感操作是否校验权限与主体一致性,是否存在越权路径 +- 会话:token 处理是否安全(存储/传输/日志/错误信息) +- 密钥:不得输出或建议写入任何密钥/token/密码/私钥/PII + +### 稳定性与性能 + +- 热路径复杂度、N+1、无界队列/循环、阻塞 I/O +- 缓存/分页/批处理是否合理,索引友好性 +- 外部依赖:熔断/退避/限流/超时/重试策略是否一致 + +### 可维护性与一致性 + +- 是否复用项目既有抽象(配置、logger、错误处理、领域模型) +- 是否引入了不必要的新依赖;若必须引入,说明理由与替代方案 +- API/DTO/Schema 是否向后兼容;破坏性变更是否标注与迁移路径 + +### 测试与验证 + +- 是否覆盖关键分支与失败路径 +- 是否新增或更新了与变更一致的测试(单测/集成/端到端) +- 是否需要补充回归用例(最小复现) + +## 输出格式(必须可执行) + +按以下结构输出,中文为默认: + +1. 结论摘要(风险:高/中/低,是否建议合并:是/否/有条件) +2. Blockers(必须修复):每条包含 + - 位置:文件 + 行号范围(若可获得) + - 问题:一句话描述 + - 影响:会导致什么 + - 修复:最小改动建议(必要时给补丁) +3. Suggestions(建议改进):同上,但不阻塞 +4. Tests(建议验证):列出要跑/要补的测试与覆盖点 + +## 约束 + +- 不凭空假设依赖或环境已存在;不确定时先在仓库中确认 +- 不输出或建议写入任何密钥/token diff --git a/DB_PERFORMANCE_ANALYSIS.md b/DB_PERFORMANCE_ANALYSIS.md new file mode 100644 index 0000000..d321a9d --- /dev/null +++ b/DB_PERFORMANCE_ANALYSIS.md @@ -0,0 +1,38 @@ +# TimescaleDB 性能测试分析报告 + +基于 `test_db.py` 对 30 万条车辆仿真数据的写入与查询测试,本次性能评估结论如下: + +## 1. 核心结论 + +**TimescaleDB 完全满足智能电动车数字孪生系统(SmartEDT)的实时采集与回放需求。** + +- **写入能力**:单线程下 **3,500+ TPS** 的写入速度远超单车采集需求(通常单车高频信号仅需 50-100Hz,即 50-100 TPS)。即使并发接入 10-20 台车,当前配置也绰绰有余。 +- **查询响应**:**毫秒级(30ms)** 的时间切片查询能力,保证了“前端实时回放”与“大屏曲线刷新”的流畅度,不会出现卡顿。 +- **存储架构**:JSONB 混合存储方案在保持了极高灵活性的同时(0.2s 过滤查询),依然维持了优秀的查询性能,验证了“结构化字段(索引)+ 半结构化 Payload(JSONB)”建模方案的正确性。 + +## 2. 详细指标分析 + +### 2.1 写入性能 (Insertion) +* **指标**:3,547.78 条/秒 (TPS) +* **场景**:单连接、Batch=1000、Python `asyncpg` 驱动。 +* **分析**: + * **满足度**:假设单车采集频率为 50Hz(即每秒 50 个数据包),当前性能理论上可支持 **70 台车同时在线采集**。 + * **瓶颈推测**:当前瓶颈主要在 Python 端的序列化与网络 IO 往返(RTT)。 + * **优化空间**:如果未来需要支持上千台车,改用 `COPY` 协议或增加并发写入 worker,吞吐量可轻松提升至 5-10 万 TPS 以上。 + +### 2.2 查询性能 (Query) + +| 查询类型 | 耗时 | 业务场景 | 评价 | +| :--- | :--- | :--- | :--- | +| **最新数据 (Latest 1000)** | **0.0301s** | 实时大屏监控、轨迹回放 | **极优**。利用 TimescaleDB 的时间分区索引,无论历史数据多大,提取“最新 N 条”永远是毫秒级。 | +| **全量计数 (Count)** | 0.0845s | 报表统计、数据完整性校验 | **优秀**。30万数据秒出,说明元数据管理高效。 | +| **JSONB 内容过滤** | 0.2137s | 复杂故障诊断、特定工况筛选 | **良好**。在没有对 JSON 内部字段建索引的情况下,0.2s 扫描 30 万条 JSON 数据属于高性能表现。 | + +## 3. 架构建议 + +基于测试结果,对后续开发提出以下建议: + +1. **保持 JSONB 方案**:当前的表结构(`ts`, `simulation_id`, `signals(JSONB)`)在性能与灵活性之间取得了完美平衡,无需拆分更多列。 +2. **索引优化**:目前 `ts` 和 `simulation_id` 已有索引。如果未来经常需要按“车速”或“故障码”过滤,建议对 JSONB 内部高频查询字段建立 **GIN 索引**。 + * *示例 SQL*:`CREATE INDEX idx_speed ON vehicle_signals USING gin ((signals->'vehicle_speed_kmh'));` +3. **保留策略**:建议配置 TimescaleDB 的 `retention_policy`,例如自动删除 30 天前的原始高频数据,只保留降采样后的聚合数据,以节省磁盘空间。 diff --git a/INSTALL_DB.md b/INSTALL_DB.md new file mode 100644 index 0000000..e28b044 --- /dev/null +++ b/INSTALL_DB.md @@ -0,0 +1,64 @@ +# 数据库安装指南 (Windows) + +由于本项目依赖 **TimescaleDB** 时序数据库插件,而该插件目前在 Windows 上**仅官方支持到 PostgreSQL 17**(暂未提供适配 PostgreSQL 18 的 Windows 安装包),因此我们需要安装 **PostgreSQL 17**。 + +## 方案一:使用 Docker(强烈推荐) + +如果您已安装 Docker Desktop,这是最简单的方法,无需配置环境。 + +1. 确保 Docker Desktop 已启动。 +2. 在项目根目录打开终端,运行: + ```powershell + docker-compose up -d + ``` +3. 完成!数据库已在端口 `5432` 启动,且已包含 TimescaleDB。 + +--- + +## 方案二:本机手动安装 + +如果您必须在 Windows 本机安装,请严格按照以下步骤操作。 + +### 第一步:安装 PostgreSQL 17 + +1. **下载**:[PostgreSQL 17.2 Windows x64 安装程序](https://get.enterprisedb.com/postgresql/postgresql-17.2-1-windows-x64.exe) + - 或者访问官网:https://www.postgresql.org/download/windows/ +2. **安装**: + - 运行安装程序。 + - **记住您设置的密码**(后续配置需要用到,建议设为 `postgres` 或修改项目配置)。 + - 端口保持默认 `5432`。 + - 安装目录建议保持默认(如 `C:\Program Files\PostgreSQL\17`)。 + - **Stack Builder**:安装结束后会询问是否运行 Stack Builder,**取消勾选**,我们不需要它。 + +### 第二步:安装 TimescaleDB 插件 + +1. **下载**:[TimescaleDB v2.23.0 for PostgreSQL 17 (Windows zip)](https://github.com/timescale/timescaledb/releases/download/2.23.0/timescaledb-postgresql-17-windows-amd64.zip) + - 备用链接:访问 [GitHub Releases](https://github.com/timescale/timescaledb/releases),找到 `timescaledb-postgresql-17-windows-amd64.zip`。 +2. **解压**: + - 将压缩包解压到一个临时文件夹。 +3. **安装**: + - 在解压后的文件夹中找到 `setup.exe`。 + - **右键 -> 以管理员身份运行**。 + - 按照提示操作: + - 输入 PostgreSQL 的安装路径(通常会自动检测)。 + - 输入 `postgres` 用户的密码。 + - 允许它修改 `postgresql.conf` 配置(输入 `y`)。 +4. **重启服务**: + - 打开 Windows 服务管理器(Win+R -> `services.msc`)。 + - 找到 `postgresql-x64-17` 服务。 + - 右键 -> **重新启动**。 + +### 第三步:验证安装 + +打开项目目录下的 `tools/check_db.py`(如果不存在可手动创建测试),或者使用 `psql`: + +```powershell +psql -U postgres +``` + +在 SQL 命令行中输入: +```sql +CREATE EXTENSION IF NOT EXISTS timescaledb; +SELECT * FROM timescaledb_information.hypertables; +``` +如果不报错,说明安装成功。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca09175 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# SmartEDT(智能电动车数字孪生系统)代码框架 + +本仓库提供一套可扩展的系统骨架代码,用于支撑《系统开发技术方案.md》中定义的后端采集处理与桌面端多屏显示能力。 + +## 目录结构 + +``` +SmartEDT/ + backend/ + frontend/ + 系统开发技术方案.md +``` + +## 后端(Python 3.13.1 + FastAPI + WebSocket + PostgreSQL/TimescaleDB) + +### 1) 环境准备 + +- 建议使用 venv: + - `python -m venv venv` + - `venv\Scripts\activate` + - `pip install -r backend/requirements.txt` + +### 2) 配置 + +- 复制示例配置: + - `copy backend\\config.ini.example backend\\config.ini` +- 按需修改 `[DATABASE].url`(示例密码为占位符 `CHANGE_ME`)。 + +也可以通过环境变量覆盖: +- `SMARTEDT_DATABASE_URL` +- `SMARTEDT_FILE_ROOT` +- `SMARTEDT_HOST` +- `SMARTEDT_PORT` + +### 3) 数据库 + +需要 PostgreSQL 并安装 TimescaleDB 扩展(数据库侧操作)。后端启动时会尝试: +- `CREATE EXTENSION IF NOT EXISTS timescaledb` +- `create_hypertable('vehicle_signals', 'ts', if_not_exists => TRUE)` + +### 4) 启动 + +- 开发启动: + - `python backend/main.py --host 127.0.0.1 --port 5000 --debug` + +接口: +- `GET /health` +- `POST /api/simulation/start` +- `POST /api/simulation/{simulation_id}/stop` +- `WS /ws` + +## 前端(Electron + Vue) + +### 1) 安装依赖 + +在 `frontend/` 目录下执行: +- `npm install` + +### 2) 开发启动(Vite + Electron) + +- `npm run dev` + +Electron 主进程会尝试启动后端: +- 默认使用 `python backend/main.py` +- 可通过 `SMARTEDT_PYTHON` 指定 Python 可执行文件路径 + diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/api/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/api/routes.py b/backend/api/routes.py new file mode 100644 index 0000000..20976d8 --- /dev/null +++ b/backend/api/routes.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from pathlib import Path + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import FileResponse + +from backend.api.schemas import HealthResponse, SimulationStartRequest, SimulationStartResponse, SimulationStopResponse +from backend.services.simulation_manager import SimulationManager +from backend.utils import safe_join + + +def get_router(simulation_manager: SimulationManager, file_root: Path) -> APIRouter: + router = APIRouter() + + @router.get("/health", response_model=HealthResponse) + async def health() -> HealthResponse: + return HealthResponse() + + @router.get("/api/devices") + async def devices(): + runtime = simulation_manager.current() + return { + "data": [ + { + "device_id": "controlbox_01", + "device_type": "mock_vehicle", + "connected": bool(runtime and runtime.status == "running"), + } + ] + } + + @router.post("/api/simulation/start", response_model=SimulationStartResponse) + async def start_simulation(body: SimulationStartRequest) -> SimulationStartResponse: + simulation_id = await simulation_manager.start(body.model_dump()) + return SimulationStartResponse(simulation_id=simulation_id) + + @router.post("/api/simulation/{simulation_id}/stop", response_model=SimulationStopResponse) + async def stop_simulation(simulation_id: str) -> SimulationStopResponse: + await simulation_manager.stop(simulation_id) + return SimulationStopResponse(simulation_id=simulation_id, status="stopped") + + @router.get("/files/{file_path:path}") + async def files(file_path: str): + try: + resolved = safe_join(file_root, file_path) + except ValueError: + raise HTTPException(status_code=400, detail="invalid path") + if not resolved.exists() or not resolved.is_file(): + raise HTTPException(status_code=404, detail="not found") + return FileResponse(str(resolved)) + + return router + diff --git a/backend/api/schemas.py b/backend/api/schemas.py new file mode 100644 index 0000000..19791c4 --- /dev/null +++ b/backend/api/schemas.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + + +class HealthResponse(BaseModel): + status: str = "ok" + + +class SimulationStartRequest(BaseModel): + scenario: str | None = None + weather: str | None = None + time_period: str | None = None + max_speed_kmh: int | None = Field(default=None, ge=0, le=300) + duration_minutes: int | None = Field(default=None, ge=1, le=360) + driver: str | None = None + extra: dict[str, Any] = Field(default_factory=dict) + + +class SimulationStartResponse(BaseModel): + simulation_id: str + + +class SimulationStopResponse(BaseModel): + simulation_id: str + status: str + diff --git a/backend/api/ws.py b/backend/api/ws.py new file mode 100644 index 0000000..4f81703 --- /dev/null +++ b/backend/api/ws.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from fastapi import WebSocket, WebSocketDisconnect + +from backend.services.broadcaster import Broadcaster + + +async def websocket_handler(ws: WebSocket, broadcaster: Broadcaster) -> None: + await ws.accept() + await broadcaster.add(ws) + try: + while True: + await ws.receive_text() + except WebSocketDisconnect: + # 客户端正常断开连接,无需打印堆栈信息 + pass + finally: + await broadcaster.remove(ws) + diff --git a/backend/build_backend.ps1 b/backend/build_backend.ps1 new file mode 100644 index 0000000..9c09616 --- /dev/null +++ b/backend/build_backend.ps1 @@ -0,0 +1,50 @@ +# Ensure we are in the backend directory +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $ScriptDir + +Write-Host "Starting Backend Build Process..." -ForegroundColor Cyan + +# Define paths +$VenvPath = Join-Path $ScriptDir "venv" +$PythonExe = Join-Path $VenvPath "Scripts\python.exe" +$PipExe = Join-Path $VenvPath "Scripts\pip.exe" +$PyInstallerExe = Join-Path $VenvPath "Scripts\pyinstaller.exe" +$SpecFile = "smartedt_backend.spec" + +# 1. Check/Create Virtual Environment +if (-not (Test-Path $VenvPath)) { + Write-Host "Virtual environment not found. Creating..." -ForegroundColor Yellow + python -m venv venv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create virtual environment." + exit 1 + } +} else { + Write-Host "Using existing virtual environment." -ForegroundColor Green +} + +# 2. Install Dependencies +Write-Host "Installing/Updating build dependencies..." -ForegroundColor Yellow +& $PipExe install -r requirements.txt +& $PipExe install -r requirements_build.txt +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to install dependencies." + exit 1 +} + +# 3. Clean previous builds +Write-Host "Cleaning up previous builds..." -ForegroundColor Yellow +if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" } +if (Test-Path "build") { Remove-Item -Recurse -Force "build" } + +# 4. Run PyInstaller +Write-Host "Running PyInstaller..." -ForegroundColor Cyan +& $PyInstallerExe --clean --noconfirm $SpecFile + +if ($LASTEXITCODE -eq 0) { + Write-Host "`nBackend build successful!" -ForegroundColor Green + Write-Host "Executable located at: $(Join-Path $ScriptDir 'dist\smartedt_backend\smartedt_backend.exe')" -ForegroundColor Green +} else { + Write-Error "`nBackend build failed!" + exit 1 +} diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/config/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/config/config.ini b/backend/config/config.ini new file mode 100644 index 0000000..e6635c6 --- /dev/null +++ b/backend/config/config.ini @@ -0,0 +1,12 @@ +[SERVER] +host = 0.0.0.0 +port = 5000 +debug = True + +[FILEPATH] +path = data + +[DATABASE] +url = postgresql+psycopg://smartedt:postgres@127.0.0.1:5432/smartedt +timescaledb = True + diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 0000000..1d6f473 --- /dev/null +++ b/backend/config/settings.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import configparser +import os +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class ServerSettings: + host: str = "0.0.0.0" + port: int = 5000 + debug: bool = False + + +@dataclass(frozen=True) +class FileSettings: + root_path: Path + + +@dataclass(frozen=True) +class DatabaseSettings: + url: str + timescaledb: bool = True + + +@dataclass(frozen=True) +class AppSettings: + server: ServerSettings + files: FileSettings + database: DatabaseSettings + + +import sys + +def _find_config_file() -> Path | None: + # Handle PyInstaller frozen state + if getattr(sys, 'frozen', False): + # If onefile, _MEIPASS. If onedir, executable dir or _internal + # With onedir, the exe is in root, internal files in _internal. + # But our spec put config in backend/config + + # Check relative to executable + exe_dir = Path(sys.executable).parent + candidates = [ + exe_dir / "backend" / "config" / "config.ini", + exe_dir / "_internal" / "backend" / "config" / "config.ini", + exe_dir / "config.ini", + ] + for path in candidates: + if path.exists(): + return path + + candidates = [ + Path(__file__).resolve().parent / "config.ini", + Path(__file__).resolve().parents[1] / "config.ini", + Path(__file__).resolve().parents[2] / "config.ini", + ] + for path in candidates: + if path.exists(): + return path + return None + + +def load_settings() -> AppSettings: + config = configparser.ConfigParser() + config_path = os.getenv("SMARTEDT_CONFIG") + if config_path: + config.read(config_path, encoding="utf-8") + else: + found = _find_config_file() + if found: + config.read(found, encoding="utf-8") + + server = ServerSettings( + host=config.get("SERVER", "host", fallback=os.getenv("SMARTEDT_HOST", "0.0.0.0")), + port=config.getint("SERVER", "port", fallback=int(os.getenv("SMARTEDT_PORT", "5000"))), + debug=config.getboolean("SERVER", "debug", fallback=os.getenv("SMARTEDT_DEBUG", "False").lower() == "true"), + ) + + default_root = Path(os.getenv("SMARTEDT_FILE_ROOT", "data")) + root_value = config.get("FILEPATH", "path", fallback=str(default_root)) + root_path = Path(root_value) + if not root_path.is_absolute(): + root_path = (Path(__file__).resolve().parents[1] / root_path).resolve() + + database_url = config.get("DATABASE", "url", fallback=os.getenv("SMARTEDT_DATABASE_URL", "")).strip() + if not database_url: + database_url = "postgresql+psycopg://smartedt:CHANGE_ME@127.0.0.1:5432/smartedt" + timescaledb = config.getboolean( + "DATABASE", + "timescaledb", + fallback=os.getenv("SMARTEDT_TIMESCALEDB", "True").lower() == "true", + ) + + return AppSettings( + server=server, + files=FileSettings(root_path=root_path), + database=DatabaseSettings(url=database_url, timescaledb=timescaledb), + ) + diff --git a/backend/database/__init__.py b/backend/database/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/database/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/database/check_db.py b/backend/database/check_db.py new file mode 100644 index 0000000..0ca5625 --- /dev/null +++ b/backend/database/check_db.py @@ -0,0 +1,66 @@ +import os +import sys + +def check_database(): + print("正在检查数据库连接...") + + # 连接参数:通过环境变量覆盖(不在输出中打印密码) + user = os.getenv("PG_USER", "smartedt") + password = os.getenv("PG_PASSWORD", "postgres") + host = os.getenv("PG_HOST", "127.0.0.1") + port = os.getenv("PG_PORT", "5432") + dbname = "smartedt" + + try: + import psycopg + conn = psycopg.connect( + dbname=dbname, + user=user, + password=password, + host=host, + port=port, + autocommit=True, + ) + server_version = getattr(conn.info, "server_version", None) + print(f"✅ 成功连接到 PostgreSQL 数据库 '{dbname}' (用户: {user}, v{server_version})") + + cur = conn.cursor() + + # 检查 TimescaleDB 扩展是否已安装 + print("正在检查 TimescaleDB 扩展...") + try: + # 尝试创建扩展(如果不存在) + cur.execute("CREATE EXTENSION IF NOT EXISTS timescaledb;") + print("✅ TimescaleDB 扩展加载成功") + + # 检查版本 + cur.execute("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';") + version = cur.fetchone() + if version: + print(f"✅ TimescaleDB 版本: {version[0]}") + else: + print("❌ 未找到 TimescaleDB 版本信息") + + except Exception as e: + print(f"❌ TimescaleDB 检查失败: {e}") + + cur.close() + conn.close() + return True + + except Exception as e: + print(f"❌ 连接失败: {e}") + print("\n可能有以下原因:") + print("1. 数据库服务未启动 (请运行 'net start postgresql-x64-17')") + print("2. 密码错误 (请设置环境变量 PG_PASSWORD)") + print("3. 端口被占用或配置不同") + return False + +if __name__ == "__main__": + try: + import psycopg # noqa: F401 + except ImportError: + print("缺少依赖 psycopg,请先安装后再运行该脚本。") + raise + + check_database() diff --git a/backend/database/engine.py b/backend/database/engine.py new file mode 100644 index 0000000..145ec5b --- /dev/null +++ b/backend/database/engine.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine + +from backend.config.settings import DatabaseSettings + + +def create_engine(settings: DatabaseSettings) -> AsyncEngine: + return create_async_engine( + settings.url, + pool_pre_ping=True, + future=True, + ) + + +def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: + return async_sessionmaker(engine, expire_on_commit=False) + diff --git a/backend/database/schema.py b/backend/database/schema.py new file mode 100644 index 0000000..b9b7ffa --- /dev/null +++ b/backend/database/schema.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import JSON, BigInteger, Boolean, Column, DateTime, Float, ForeignKey, Index, String, Table, text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + pass + + +class Simulation(Base): + __tablename__ = "simulations" + + simulation_id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="仿真 ID") + status: Mapped[str] = mapped_column(String(32), index=True, comment="仿真状态(running/stopped 等)") + started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True, comment="开始时间(UTC)") + ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, comment="结束时间(UTC)") + scenario_name: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True, comment="仿真场景名称") + scenario_config: Mapped[dict] = mapped_column(JSON, default=dict, comment="仿真场景配置(JSON)") + config_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, comment="配置创建时间(UTC)") + operator: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True, comment="仿真操作员") + archived: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否归档") + + +vehicle_signals = Table( + "sim_vehicle_signals", + Base.metadata, + Column("ts", DateTime(timezone=True), nullable=False, index=True, comment="信号采样时间(UTC)"), + Column("simulation_id", String(64), nullable=False, index=True, comment="仿真 ID"), + Column("device_id", String(64), nullable=False, index=True, comment="设备 ID"), + Column("seq", BigInteger, nullable=False, comment="信号序列号(单仿真内递增)"), + Column("signals", JSONB, nullable=False, comment="车辆信号载荷(JSONB)"), + Index("idx_vehicle_signals_sim_ts", "simulation_id", "ts"), + comment="车辆信号时序数据(TimescaleDB hypertable)", +) + +unity_vehicle_frames = Table( + "sim_unity_vehicle_frames", + Base.metadata, + Column("ts", DateTime(timezone=True), nullable=False, index=True, comment="帧时间(UTC)"), + Column("simulation_id", String(64), nullable=False, index=True, comment="仿真 ID"), + Column("vehicle_id", String(64), nullable=False, index=True, comment="虚拟车辆 ID"), + Column("seq", BigInteger, nullable=False, comment="帧序号(单仿真单车内递增)"), + Column("pos_x", Float, nullable=False, comment="位置 X(世界坐标)"), + Column("pos_y", Float, nullable=False, comment="位置 Y(世界坐标)"), + Column("pos_z", Float, nullable=False, comment="位置 Z(世界坐标)"), + Column("rot_x", Float, nullable=False, comment="旋转四元数 X"), + Column("rot_y", Float, nullable=False, comment="旋转四元数 Y"), + Column("rot_z", Float, nullable=False, comment="旋转四元数 Z"), + Column("rot_w", Float, nullable=False, comment="旋转四元数 W"), + Column("lin_vel_x", Float, nullable=True, comment="线速度 X(可选)"), + Column("lin_vel_y", Float, nullable=True, comment="线速度 Y(可选)"), + Column("lin_vel_z", Float, nullable=True, comment="线速度 Z(可选)"), + Column("ang_vel_x", Float, nullable=True, comment="角速度 X(可选)"), + Column("ang_vel_y", Float, nullable=True, comment="角速度 Y(可选)"), + Column("ang_vel_z", Float, nullable=True, comment="角速度 Z(可选)"), + Column("controls", JSONB, nullable=True, comment="控制量(油门/刹车/方向/档位等,JSONB)"), + Column("extra", JSONB, nullable=True, comment="扩展字段(仿真引擎自定义,JSONB)"), + Index("idx_unity_frames_sim_vehicle_ts", "simulation_id", "vehicle_id", "ts"), + comment="虚拟车辆驱动仿真帧数据(用于 Unity 车辆模型运动与回放,TimescaleDB hypertable)", +) + +screen_recordings = Table( + "sim_screen_videos", + Base.metadata, + Column("video_id", String(64), primary_key=True, comment="录制文件记录 ID"), + Column("simulation_id", String(64), nullable=False, index=True, comment="仿真 ID"), + Column("screen_type", String(32), nullable=False, index=True, comment="屏幕类型(big_screen/vehicle_screen 等)"), + Column("source_name", String(64), nullable=True, index=True, comment="录制源名称(可选,如设备号/通道号)"), + Column("status", String(32), nullable=False, index=True, comment="状态(recording/ready/failed 等)"), + Column("relative_path", String(1024), nullable=False, comment="相对文件根目录的路径(用于下载/归档)"), + Column("file_name", String(255), nullable=True, comment="文件名(可选)"), + Column("format", String(32), nullable=True, comment="容器格式(mp4/mkv 等)"), + Column("codec", String(64), nullable=True, comment="编码信息(H264/H265 等)"), + Column("width", BigInteger, nullable=True, comment="视频宽度(像素)"), + Column("height", BigInteger, nullable=True, comment="视频高度(像素)"), + Column("fps", Float, nullable=True, comment="帧率(可选)"), + Column("duration_ms", BigInteger, nullable=True, comment="时长(毫秒,可选)"), + Column("size_bytes", BigInteger, nullable=True, comment="文件大小(字节,可选)"), + Column("recorded_started_at", DateTime(timezone=True), nullable=True, index=True, comment="录制开始时间(UTC,可选)"), + Column("recorded_ended_at", DateTime(timezone=True), nullable=True, index=True, comment="录制结束时间(UTC,可选)"), + Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="记录创建时间(UTC)"), + Column("extra", JSONB, nullable=True, comment="扩展信息(JSONB)"), + Index("idx_screen_recordings_sim_screen_created", "simulation_id", "screen_type", "created_at"), + Index("idx_screen_recordings_sim_screen_time", "simulation_id", "screen_type", "recorded_started_at"), + comment="仿真过程屏幕录制文件元数据(显示大屏/车载屏等)", +) + +sys_role = Table( + "sys_role", + Base.metadata, + Column("role_id", String(64), primary_key=True, comment="角色 ID"), + Column("role_name", String(64), nullable=False, unique=True, index=True, comment="角色名称(唯一)"), + Column("role_desc", String(255), nullable=True, comment="角色描述"), + Column("is_active", Boolean, nullable=False, server_default=text("TRUE"), index=True, comment="是否启用"), + Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间(UTC)"), + Column("updated_at", DateTime(timezone=True), nullable=True, comment="更新时间(UTC)"), + Column("extra", JSONB, nullable=True, comment="扩展信息(JSONB)"), + comment="系统角色", +) + +sys_permission = Table( + "sys_permission", + Base.metadata, + Column("perm_code", String(128), primary_key=True, comment="权限编码(唯一)"), + Column("perm_name", String(128), nullable=False, index=True, comment="权限名称"), + Column("perm_group", String(64), nullable=True, index=True, comment="权限分组(可选)"), + Column("perm_desc", String(255), nullable=True, comment="权限描述"), + Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间(UTC)"), + comment="系统功能权限", +) + +sys_role_permission = Table( + "sys_role_permission", + Base.metadata, + Column("role_id", String(64), ForeignKey("sys_role.role_id", ondelete="CASCADE"), primary_key=True, comment="角色 ID"), + Column("perm_code", String(128), ForeignKey("sys_permission.perm_code", ondelete="CASCADE"), primary_key=True, comment="权限编码"), + Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间(UTC)"), + Index("idx_sys_role_permission_role", "role_id"), + Index("idx_sys_role_permission_perm", "perm_code"), + comment="角色功能权限关联表", +) + +sys_user = Table( + "sys_user", + Base.metadata, + Column("user_id", String(64), primary_key=True, comment="用户 ID"), + Column("username", String(64), nullable=False, unique=True, index=True, comment="登录名(唯一)"), + Column("display_name", String(64), nullable=True, index=True, comment="显示名称"), + Column("password_hash", String(255), nullable=False, comment="密码哈希"), + Column("role_id", String(64), ForeignKey("sys_role.role_id"), nullable=False, index=True, comment="所属角色 ID"), + Column("is_active", Boolean, nullable=False, server_default=text("TRUE"), index=True, comment="是否启用"), + Column("last_login_at", DateTime(timezone=True), nullable=True, index=True, comment="最近登录时间(UTC)"), + Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间(UTC)"), + Column("updated_at", DateTime(timezone=True), nullable=True, comment="更新时间(UTC)"), + Column("extra", JSONB, nullable=True, comment="扩展信息(JSONB)"), + comment="系统用户(含所属角色)", +) + +sys_logs = Table( + "sys_logs", + Base.metadata, + Column("log_id", BigInteger, primary_key=True, comment="日志 ID"), + Column("ts", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="操作时间(UTC)"), + Column("user_id", String(64), nullable=True, index=True, comment="用户 ID(可为空,如匿名)"), + Column("username", String(64), nullable=True, index=True, comment="登录名快照(可选)"), + Column("role_id", String(64), nullable=True, index=True, comment="角色 ID 快照(可选)"), + Column("action", String(128), nullable=False, index=True, comment="操作动作(如 login/start_simulation)"), + Column("resource", String(255), nullable=True, index=True, comment="资源标识(如 URL/对象 ID)"), + Column("success", Boolean, nullable=False, server_default=text("TRUE"), index=True, comment="是否成功"), + Column("ip", String(64), nullable=True, comment="客户端 IP(可选)"), + Column("user_agent", String(512), nullable=True, comment="User-Agent(可选)"), + Column("detail", JSONB, nullable=True, comment="操作明细(JSONB,可选)"), + Index("idx_sys_logs_user_ts", "user_id", "ts"), + Index("idx_sys_logs_action_ts", "action", "ts"), + comment="系统操作日志", +) + + +server_metrics = Table( + "server_metrics", + Base.metadata, + Column("ts", DateTime(timezone=True), nullable=False, index=True, comment="采样时间(UTC)"), + Column("host_name", String(64), nullable=False, index=True, comment="主机名"), + Column("cpu_usage_percent", JSONB, nullable=False, comment="CPU 使用率(百分比,JSONB)"), + Column("memory_usage_bytes", JSONB, nullable=False, comment="内存使用情况(字节,JSONB)"), + Column("disk_usage_bytes", JSONB, nullable=True, comment="磁盘使用情况(字节,JSONB)"), + Index("idx_server_metrics_host_ts", "host_name", "ts"), + comment="服务器监控指标时序数据(TimescaleDB hypertable)", +) + + +async def init_schema(engine) -> None: + from sqlalchemy.ext.asyncio import AsyncEngine + + if not isinstance(engine, AsyncEngine): + raise TypeError("engine must be AsyncEngine") + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await conn.execute(text("ALTER TABLE simulations ADD COLUMN IF NOT EXISTS scenario_name VARCHAR(255)")) + await conn.execute(text("ALTER TABLE simulations ADD COLUMN IF NOT EXISTS config_created_at TIMESTAMPTZ")) + await conn.execute(text("ALTER TABLE simulations ADD COLUMN IF NOT EXISTS operator VARCHAR(64)")) + await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_simulations_scenario_name ON simulations (scenario_name)")) + await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_simulations_config_created_at ON simulations (config_created_at)")) + await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_simulations_operator ON simulations (operator)")) + + +async def init_timescaledb(engine) -> None: + async with engine.begin() as conn: + await conn.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb")) + await conn.execute( + text( + "SELECT create_hypertable('sim_vehicle_signals', 'ts', if_not_exists => TRUE)" + ) + ) + await conn.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_vehicle_signals_sim_ts_desc ON sim_vehicle_signals (simulation_id, ts DESC)" + ) + ) + await conn.execute( + text( + "SELECT create_hypertable('server_metrics', 'ts', if_not_exists => TRUE)" + ) + ) + await conn.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_server_metrics_host_ts_desc ON server_metrics (host_name, ts DESC)" + ) + ) + await conn.execute( + text( + "SELECT create_hypertable('sim_unity_vehicle_frames', 'ts', if_not_exists => TRUE)" + ) + ) + await conn.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_unity_frames_sim_vehicle_ts_desc ON sim_unity_vehicle_frames (simulation_id, vehicle_id, ts DESC)" + ) + ) diff --git a/backend/database/test_db.py b/backend/database/test_db.py new file mode 100644 index 0000000..c8674ae --- /dev/null +++ b/backend/database/test_db.py @@ -0,0 +1,153 @@ +import asyncio +import os +import time +import json +import random +from datetime import datetime, timezone +from sqlalchemy import insert, select, text +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.engine.url import make_url +from backend.database.schema import vehicle_signals, Simulation, init_schema, init_timescaledb +from backend.config.settings import load_settings + +# 模拟数据生成 +def generate_payload(): + return { + "steering_wheel_angle_deg": round(random.uniform(-450, 450), 1), + "brake_pedal_travel_mm": round(random.uniform(0, 100), 1), + "throttle_pedal_travel_mm": round(random.uniform(0, 100), 1), + "gear": random.choice(["P", "N", "D", "R"]), + "handbrake": random.choice([0, 1]), + "vehicle_speed_kmh": round(random.uniform(0, 180), 1), + "wheel_speed_rpm": { + "FL": random.randint(0, 2000), + "FR": random.randint(0, 2000), + "RL": random.randint(0, 2000), + "RR": random.randint(0, 2000) + }, + "lights": { + "left_turn": random.choice([0, 1]), + "right_turn": random.choice([0, 1]), + "hazard": random.choice([0, 1]), + "brake": random.choice([0, 1]) + }, + "soc_percent": round(random.uniform(0, 100), 1), + "voltage_v": round(random.uniform(300, 400), 1), + "current_a": round(random.uniform(-50, 200), 1), + "temperature_c": round(random.uniform(20, 80), 1) + } + +def _redact_url(url: str) -> str: + try: + parsed = make_url(url) + if parsed.password: + parsed = parsed.set(password="***") + return str(parsed) + except Exception: + return url + +async def run_test(): + settings = load_settings() + + db_url = os.getenv("SMARTEDT_TEST_DATABASE_URL", settings.database.url).strip() + print(f"Connecting to DB: {_redact_url(db_url)}") + engine = create_async_engine(db_url, echo=False) + + # 0. 初始化表结构 (如果不存在) + print("Initializing schema...") + await init_schema(engine) + try: + await init_timescaledb(engine) + print("Schema and TimescaleDB initialized.") + except Exception as e: + print(f"TimescaleDB init warning (might already exist): {e}") + + # 1. 准备测试数据 + total_records = int(os.getenv("SMARTEDT_TEST_RECORDS", "300000")) + batch_size = int(os.getenv("SMARTEDT_TEST_BATCH_SIZE", "1000")) + simulation_id = f"TEST_SIM_{int(time.time())}" + device_id = "test_device_01" + + print(f"Generating {total_records} records for simulation {simulation_id}...") + + print("Starting insertion test...") + + # 2. 插入性能测试 + insert_start_time = time.time() + + async with engine.begin() as conn: + # 分批插入 + for base_seq in range(0, total_records, batch_size): + batch = [] + end_seq = min(base_seq + batch_size, total_records) + for seq in range(base_seq, end_seq): + batch.append( + { + "ts": datetime.now(timezone.utc), + "simulation_id": simulation_id, + "device_id": device_id, + "seq": seq, + "signals": generate_payload(), + } + ) + await conn.execute(insert(vehicle_signals), batch) + if end_seq % 50000 == 0: + print(f"Inserted {end_seq} records...") + + insert_end_time = time.time() + insert_duration = insert_end_time - insert_start_time + print(f"\n✅ Insertion Test Complete:") + print(f"Total Records: {total_records}") + print(f"Time Taken: {insert_duration:.4f} seconds") + print(f"Throughput: {total_records / insert_duration:.2f} records/sec") + + # 3. 查询性能测试 + print("\nStarting query performance test...") + + # 3.1 简单计数查询 + query_start = time.time() + async with engine.connect() as conn: + result = await conn.execute( + select(text("count(*)")).select_from(vehicle_signals).where(vehicle_signals.c.simulation_id == simulation_id) + ) + count = result.scalar() + query_end = time.time() + print(f"Query 1 (Count): Found {count} records in {query_end - query_start:.4f} seconds") + + # 3.2 复杂 JSONB 查询 (查询车速 > 100 的记录数) + # 注意:JSONB 查询语法取决于数据库和 SQLAlchemy 版本,这里使用 text() 以确保兼容性 + query_start = time.time() + async with engine.connect() as conn: + # 查询 signals->>'vehicle_speed_kmh' > 100 + stmt = text( + "SELECT count(*) FROM vehicle_signals " + "WHERE simulation_id = :sim_id " + "AND (signals->>'vehicle_speed_kmh')::float > 100" + ) + result = await conn.execute(stmt, {"sim_id": simulation_id}) + high_speed_count = result.scalar() + query_end = time.time() + print(f"Query 2 (JSONB Filter): Found {high_speed_count} records with speed > 100 in {query_end - query_start:.4f} seconds") + + # 3.3 时间范围查询 (查询最近 1000 条) + query_start = time.time() + async with engine.connect() as conn: + stmt = ( + select(vehicle_signals) + .where(vehicle_signals.c.simulation_id == simulation_id) + .order_by(vehicle_signals.c.ts.desc()) + .limit(1000) + ) + result = await conn.execute(stmt) + rows = result.fetchall() + query_end = time.time() + print(f"Query 3 (Time Range Limit): Retrieved {len(rows)} records in {query_end - query_start:.4f} seconds") + + await engine.dispose() + +if __name__ == "__main__": + # 确保在 Windows 上正确运行 asyncio + if hasattr(asyncio, 'WindowsSelectorEventLoopPolicy'): + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + asyncio.run(run_test()) diff --git a/backend/device/__init__.py b/backend/device/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/device/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/device/base.py b/backend/device/base.py new file mode 100644 index 0000000..244b1e0 --- /dev/null +++ b/backend/device/base.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class DeviceInfo: + device_id: str + device_type: str + connected: bool + + +class DeviceAdapter: + device_id: str + device_type: str + + async def connect(self) -> None: + raise NotImplementedError + + async def disconnect(self) -> None: + raise NotImplementedError + + async def is_connected(self) -> bool: + raise NotImplementedError + diff --git a/backend/device/mock_vehicle.py b/backend/device/mock_vehicle.py new file mode 100644 index 0000000..f5242b3 --- /dev/null +++ b/backend/device/mock_vehicle.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import random +from dataclasses import dataclass + +from backend.device.base import DeviceAdapter + + +@dataclass(frozen=True) +class VehicleSignalPayload: + steering_wheel_angle_deg: float + brake_pedal_travel_mm: float + throttle_pedal_travel_mm: float + gear: str + handbrake: int + vehicle_speed_kmh: float + wheel_speed_rpm: dict + lights: dict + soc_percent: float + voltage_v: float + current_a: float + temperature_c: float + + def to_dict(self) -> dict: + return { + "steering_wheel_angle_deg": self.steering_wheel_angle_deg, + "brake_pedal_travel_mm": self.brake_pedal_travel_mm, + "throttle_pedal_travel_mm": self.throttle_pedal_travel_mm, + "gear": self.gear, + "handbrake": self.handbrake, + "vehicle_speed_kmh": self.vehicle_speed_kmh, + "wheel_speed_rpm": self.wheel_speed_rpm, + "lights": self.lights, + "soc_percent": self.soc_percent, + "voltage_v": self.voltage_v, + "current_a": self.current_a, + "temperature_c": self.temperature_c, + } + + +class MockVehicleDevice(DeviceAdapter): + def __init__(self, device_id: str = "controlbox_01") -> None: + self.device_id = device_id + self.device_type = "mock_vehicle" + self._connected = False + + async def connect(self) -> None: + self._connected = True + + async def disconnect(self) -> None: + self._connected = False + + async def is_connected(self) -> bool: + return self._connected + + def sample(self) -> VehicleSignalPayload: + steering = random.uniform(-180.0, 180.0) + brake = max(0.0, random.gauss(2.0, 1.0)) + throttle = max(0.0, random.gauss(15.0, 5.0)) + gear = random.choice(["P", "N", "D", "S"]) + handbrake = 1 if gear == "P" else 0 + speed = max(0.0, random.gauss(40.0, 10.0)) if gear in {"D", "S"} else 0.0 + rpm = int(speed * 9 + random.uniform(-5, 5)) + wheel = {"FL": rpm, "FR": rpm, "RL": rpm, "RR": rpm} + lights = { + "left_turn": int(random.random() < 0.05), + "right_turn": int(random.random() < 0.05), + "hazard": int(random.random() < 0.02), + "brake": int(brake > 5.0), + } + soc = max(0.0, min(100.0, 80.0 - random.random() * 0.1)) + voltage = 360.0 + random.uniform(-0.5, 0.5) + current = max(0.0, random.gauss(15.0, 2.0)) + temp = 28.0 + random.uniform(-0.2, 0.2) + return VehicleSignalPayload( + steering_wheel_angle_deg=steering, + brake_pedal_travel_mm=brake, + throttle_pedal_travel_mm=throttle, + gear=gear, + handbrake=handbrake, + vehicle_speed_kmh=speed, + wheel_speed_rpm=wheel, + lights=lights, + soc_percent=soc, + voltage_v=voltage, + current_a=current, + temperature_c=temp, + ) + diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..c49f64d --- /dev/null +++ b/backend/main.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import argparse +import asyncio +import logging +import multiprocessing +import platform +import sys +from pathlib import Path + +# 修复 Windows 下 psycopg 异步连接的 Event Loop 问题 +if platform.system() == "Windows": + # 1. 强制设置 SelectorEventLoopPolicy + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + # 2. Monkeypatch 阻止 uvicorn 覆盖策略 (uvicorn 默认在 Windows 上强制使用 Proactor) + # 这对 psycopg 3 (SQLAlchemy async) 是必须的,因为 Proactor 不支持 add_reader/add_writer + original_set_policy = asyncio.set_event_loop_policy + def patched_set_policy(policy): + # 如果尝试设置 Proactor,则忽略并保持 Selector + if isinstance(policy, asyncio.WindowsProactorEventLoopPolicy): + return + original_set_policy(policy) + asyncio.set_event_loop_policy = patched_set_policy + +import uvicorn +from dotenv import load_dotenv +from fastapi import FastAPI, WebSocket + +from contextlib import asynccontextmanager + +from backend.config.settings import load_settings +from backend.database.engine import create_engine, create_session_factory +from backend.database.schema import init_schema, init_timescaledb +from backend.services.broadcaster import Broadcaster +from backend.services.simulation_manager import SimulationManager +from backend.services.server_monitor import ServerMonitorService +from backend.device.mock_vehicle import MockVehicleDevice +from backend.api import routes, ws +from backend.utils import configure_logging + +logger = logging.getLogger("backend") + +def _default_backend_log_file() -> Path | None: + if getattr(sys, "frozen", False): + exe_dir = Path(sys.executable).resolve().parent + return exe_dir / "logs" / "backend.log" + return None + +def _force_windows_selector_event_loop_for_uvicorn() -> None: + if platform.system() != "Windows": + return + try: + import uvicorn.loops.asyncio as uvicorn_asyncio_loop + except Exception: + return + + def _selector_loop_factory(use_subprocess: bool = False): + return asyncio.SelectorEventLoop + + uvicorn_asyncio_loop.asyncio_loop_factory = _selector_loop_factory + +# 全局单例容器(简单实现) +class Container: + def __init__(self): + load_dotenv() + self.settings = load_settings() + configure_logging( + "INFO" if not self.settings.server.debug else "DEBUG", + log_file=_default_backend_log_file(), + ) + + self.file_root = self.settings.files.root_path + self.file_root.mkdir(parents=True, exist_ok=True) + + self.engine = create_engine(self.settings.database) + self.session_factory = create_session_factory(self.engine) + self.broadcaster = Broadcaster() + + # 实例化服务 + self.simulation_manager = SimulationManager( + self.session_factory, + self.broadcaster + ) + + # 实例化监控服务 + self.server_monitor = ServerMonitorService( + self.session_factory, + self.broadcaster + ) + +container = Container() + +@asynccontextmanager +async def lifespan(app: FastAPI): + # 启动前初始化 + await init_schema(container.engine) + if container.settings.database.timescaledb: + try: + await init_timescaledb(container.engine) + except Exception as e: + # 可能是已存在或权限问题,仅打印日志 + logger.warning("TimescaleDB init warning: %s", e) + + # 注册默认设备 + mock_dev = MockVehicleDevice(device_id="mock_01") + await container.simulation_manager.register_device(mock_dev) + + # 启动监控服务 + await container.server_monitor.start() + + yield + + # 停机清理 + await container.server_monitor.stop() + await container.simulation_manager.stop() + await container.engine.dispose() + +app = FastAPI(title="SmartEDT Backend", version="0.1.0", lifespan=lifespan) +app.include_router(routes.get_router(simulation_manager=container.simulation_manager, file_root=container.file_root)) + +@app.websocket("/ws") +async def ws_endpoint(websocket: WebSocket): + await ws.websocket_handler(websocket, broadcaster=container.broadcaster) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--host", default=None) + parser.add_argument("--port", type=int, default=None) + parser.add_argument("--debug", action="store_true") + args = parser.parse_args() + + settings = load_settings() + host = args.host or settings.server.host + port = args.port or settings.server.port + debug = args.debug or settings.server.debug + + _force_windows_selector_event_loop_for_uvicorn() + + if getattr(sys, 'frozen', False): + uvicorn.run(app, host=host, port=port, log_level="debug" if debug else "info") + else: + uvicorn.run("backend.main:app", host=host, port=port, reload=debug, log_level="debug" if debug else "info") + + +if __name__ == "__main__": + multiprocessing.freeze_support() + main() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..c0fe056 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +sqlalchemy[asyncio]>=2.0.30 +psycopg[binary]>=3.2.0 +python-dotenv>=1.0.1 +pydantic>=2.8.0 +psutil>=5.9.0 + diff --git a/backend/requirements_build.txt b/backend/requirements_build.txt new file mode 100644 index 0000000..07bf186 --- /dev/null +++ b/backend/requirements_build.txt @@ -0,0 +1,2 @@ +pyinstaller>=6.9.0 + diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/broadcaster.py b/backend/services/broadcaster.py new file mode 100644 index 0000000..c1a0a7f --- /dev/null +++ b/backend/services/broadcaster.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import asyncio +from typing import Any + +from starlette.websockets import WebSocket + + +class Broadcaster: + def __init__(self) -> None: + self._clients: set[WebSocket] = set() + self._lock = asyncio.Lock() + + async def add(self, ws: WebSocket) -> None: + async with self._lock: + self._clients.add(ws) + + async def remove(self, ws: WebSocket) -> None: + async with self._lock: + self._clients.discard(ws) + + async def broadcast_json(self, message: dict[str, Any]) -> None: + async with self._lock: + clients = list(self._clients) + for ws in clients: + try: + await ws.send_json(message) + except Exception: + await self.remove(ws) + diff --git a/backend/services/server_monitor.py b/backend/services/server_monitor.py new file mode 100644 index 0000000..46e175f --- /dev/null +++ b/backend/services/server_monitor.py @@ -0,0 +1,127 @@ +import asyncio +import time +import logging +import psutil +import socket +import platform +from datetime import datetime, timezone +from sqlalchemy import insert +from sqlalchemy.ext.asyncio import async_sessionmaker + +from backend.database.schema import server_metrics +from backend.services.broadcaster import Broadcaster + +logger = logging.getLogger("backend.monitor") + +class ServerMonitorService: + def __init__(self, session_factory: async_sessionmaker, broadcaster: Broadcaster): + self._session_factory = session_factory + self._broadcaster = broadcaster + self._host_name = socket.gethostname() + self._running = False + self._task = None + self._sample_interval = 1.0 / 50.0 # 50Hz (20ms) + self._report_interval = 1.0 / 10.0 # 10Hz (100ms) + self._last_report_time = 0.0 + + # Buffer for downsampling + self._buffer_cpu = [] + self._buffer_mem = [] + + async def start(self): + if self._running: + return + self._running = True + self._task = asyncio.create_task(self._run_loop()) + logger.info("ServerMonitorService started") + + async def stop(self): + self._running = False + if self._task: + try: + await self._task + except asyncio.CancelledError: + pass + logger.info("ServerMonitorService stopped") + + async def _run_loop(self): + loop = asyncio.get_running_loop() + next_time = loop.time() + + while self._running: + # High frequency sampling (50Hz) + # psutil.cpu_percent(interval=None) is non-blocking + cpu_percent = psutil.cpu_percent(interval=None) + mem = psutil.virtual_memory() + + self._buffer_cpu.append(cpu_percent) + self._buffer_mem.append(mem) + + current_time = loop.time() + + # Check if it's time to report (10Hz) + if current_time - self._last_report_time >= self._report_interval: + await self._process_and_report() + self._last_report_time = current_time + self._buffer_cpu.clear() + self._buffer_mem.clear() + + # Precise timing control + next_time += self._sample_interval + sleep_time = next_time - loop.time() + if sleep_time > 0: + await asyncio.sleep(sleep_time) + else: + # If we are lagging, yield execution but don't sleep + await asyncio.sleep(0) + + async def _process_and_report(self): + if not self._buffer_cpu: + return + + # Downsampling: Calculate average of buffered samples + avg_cpu = sum(self._buffer_cpu) / len(self._buffer_cpu) + + # Take the latest memory reading (memory doesn't fluctuate as fast as CPU) + last_mem = self._buffer_mem[-1] + + payload = { + "ts": datetime.now(timezone.utc).isoformat(), + "host_name": self._host_name, + "cpu_usage_percent": { + "total": round(avg_cpu, 2), + # Note: per-core usage is expensive to query at 50Hz, so we only track total here + # or we could sample per-core at lower frequency + }, + "memory_usage_bytes": { + "total": last_mem.total, + "available": last_mem.available, + "used": last_mem.used, + "percent": last_mem.percent + }, + "disk_usage_bytes": {} # Optional: disk usage changes slowly, maybe check every 1s + } + + # 1. Broadcast via WebSocket (10Hz) + await self._broadcaster.broadcast_json({ + "type": "server.metrics", + "payload": payload + }) + + # 2. Persist to Database (10Hz) + # Note: In production, consider batching inserts further (e.g., every 1s) + # to reduce DB load, but 10Hz single insert is manageable for TimescaleDB. + async with self._session_factory() as session: + try: + stmt = insert(server_metrics).values( + ts=datetime.fromisoformat(payload["ts"]), + host_name=payload["host_name"], + cpu_usage_percent=payload["cpu_usage_percent"], + memory_usage_bytes=payload["memory_usage_bytes"], + disk_usage_bytes=payload["disk_usage_bytes"] + ) + await session.execute(stmt) + await session.commit() + except Exception as e: + logger.warning("Failed to persist server metrics: %s", e) + # Don't raise, keep monitoring running diff --git a/backend/services/simulation_manager.py b/backend/services/simulation_manager.py new file mode 100644 index 0000000..6508054 --- /dev/null +++ b/backend/services/simulation_manager.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import asyncio +import logging +import secrets +from dataclasses import dataclass +from typing import Any + +from sqlalchemy import insert +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +from backend.database.schema import Simulation, vehicle_signals +from backend.device.mock_vehicle import MockVehicleDevice +from backend.services.broadcaster import Broadcaster +from backend.utils import utc_now + + +logger = logging.getLogger("backend.simulation") + + +@dataclass +class SimulationRuntime: + simulation_id: str + status: str + task: asyncio.Task | None = None + + +class SimulationManager: + def __init__( + self, + session_factory: async_sessionmaker[AsyncSession], + broadcaster: Broadcaster, + ) -> None: + self._session_factory = session_factory + self._broadcaster = broadcaster + self._runtime: SimulationRuntime | None = None + self._device = MockVehicleDevice() + self._seq = 0 + + def current(self) -> SimulationRuntime | None: + return self._runtime + + async def register_device(self, device: MockVehicleDevice) -> None: + self._device = device + + async def start(self, scenario_config: dict[str, Any]) -> str: + if self._runtime and self._runtime.status == "running": + return self._runtime.simulation_id + + simulation_id = "SIM" + utc_now().strftime("%Y%m%d%H%M%S") + secrets.token_hex(2).upper() + started_at = utc_now() + scenario_name = scenario_config.get("scenario") + operator = scenario_config.get("driver") or scenario_config.get("operator") + config_created_at = started_at + + async with self._session_factory() as session: + session.add( + Simulation( + simulation_id=simulation_id, + status="running", + started_at=started_at, + ended_at=None, + scenario_name=scenario_name, + scenario_config=scenario_config, + config_created_at=config_created_at, + operator=operator, + archived=False, + ) + ) + await session.commit() + + await self._device.connect() + self._runtime = SimulationRuntime(simulation_id=simulation_id, status="running") + self._runtime.task = asyncio.create_task(self._run_loop(simulation_id)) + await self._broadcaster.broadcast_json( + {"type": "simulation.status", "ts": started_at.timestamp(), "simulation_id": simulation_id, "payload": {"status": "running"}} + ) + return simulation_id + + async def stop(self, simulation_id: str) -> None: + runtime = self._runtime + if not runtime or runtime.simulation_id != simulation_id: + return + + runtime.status = "stopping" + if runtime.task: + runtime.task.cancel() + try: + await runtime.task + except asyncio.CancelledError: + pass + + await self._device.disconnect() + ended_at = utc_now() + + async with self._session_factory() as session: + sim = await session.get(Simulation, simulation_id) + if sim: + sim.status = "stopped" + sim.ended_at = ended_at + await session.commit() + + await self._broadcaster.broadcast_json( + {"type": "simulation.status", "ts": ended_at.timestamp(), "simulation_id": simulation_id, "payload": {"status": "stopped"}} + ) + self._runtime = None + + async def _run_loop(self, simulation_id: str) -> None: + try: + while True: + await asyncio.sleep(0.05) + if not await self._device.is_connected(): + continue + + self._seq += 1 + ts = utc_now() + payload = self._device.sample().to_dict() + message = { + "type": "vehicle.signal", + "ts": ts.timestamp(), + "simulation_id": simulation_id, + "device_id": self._device.device_id, + "seq": self._seq, + "payload": payload, + } + await self._broadcaster.broadcast_json(message) + await self._persist_signal(ts, simulation_id, self._device.device_id, self._seq, payload) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("simulation loop crashed") + + async def _persist_signal(self, ts, simulation_id: str, device_id: str, seq: int, signals: dict[str, Any]) -> None: + async with self._session_factory() as session: + await session.execute( + insert(vehicle_signals).values( + ts=ts, + simulation_id=simulation_id, + device_id=device_id, + seq=seq, + signals=signals, + ) + ) + await session.commit() diff --git a/backend/smartedt_backend.spec b/backend/smartedt_backend.spec new file mode 100644 index 0000000..ea48d07 --- /dev/null +++ b/backend/smartedt_backend.spec @@ -0,0 +1,95 @@ +# -*- mode: python ; coding: utf-8 -*- + +from PyInstaller.utils.hooks import collect_all + +datas = [ + ('config/config.ini', 'backend/config') +] +binaries = [] +hiddenimports = [ + 'uvicorn.logging', + 'uvicorn.loops', + 'uvicorn.loops.auto', + 'uvicorn.protocols', + 'uvicorn.protocols.http', + 'uvicorn.protocols.http.auto', + 'uvicorn.protocols.websockets', + 'uvicorn.protocols.websockets.auto', + 'uvicorn.lifespan', + 'uvicorn.lifespan.on', + 'backend.api', + 'backend.api.routes', + 'backend.api.schemas', + 'backend.api.ws', + 'backend.config', + 'backend.config.settings', + 'backend.database', + 'backend.database.engine', + 'backend.database.schema', + 'backend.device', + 'backend.device.base', + 'backend.device.mock_vehicle', + 'backend.services', + 'backend.services.broadcaster', + 'backend.services.server_monitor', + 'backend.services.simulation_manager', + 'backend.utils', + 'psycopg', + 'psycopg_binary', + 'sqlalchemy.ext.asyncio', + 'sqlalchemy.dialects.postgresql', + 'asyncpg', # Just in case, though we use psycopg +] + +tmp_ret = collect_all('uvicorn') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] + +tmp_ret = collect_all('psycopg') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] + +block_cipher = None + +a = Analysis( + ['main.py'], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='smartedt_backend', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='smartedt_backend', +) diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 0000000..382163a --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import logging +import os +from datetime import datetime, timezone +from logging.handlers import TimedRotatingFileHandler +from pathlib import Path + + +def utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def configure_logging(level: str, log_file: Path | None = None) -> None: + level_value = getattr(logging, level.upper(), logging.INFO) + logging_handlers: list[logging.Handler] = [logging.StreamHandler()] + if log_file is not None: + log_file.parent.mkdir(parents=True, exist_ok=True) + rotating_handler = TimedRotatingFileHandler( + log_file, + when="midnight", + interval=1, + backupCount=90, + encoding="utf-8", + delay=True, + ) + rotating_handler.suffix = "%Y-%m-%d" + logging_handlers.append(rotating_handler) + logging.basicConfig( + level=level_value, + format="%(asctime)s %(levelname)s %(name)s %(message)s", + handlers=logging_handlers, + force=True, + ) + for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access", "uvicorn.asgi"): + logger = logging.getLogger(logger_name) + logger.handlers = [] + logger.propagate = True + + +def project_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def safe_join(root: Path, untrusted_path: str) -> Path: + if untrusted_path.startswith(("\\\\", "//")): + raise ValueError("UNC path is not allowed") + if os.path.isabs(untrusted_path): + raise ValueError("Absolute path is not allowed") + candidate = (root / untrusted_path).resolve() + root_resolved = root.resolve() + if root_resolved not in candidate.parents and candidate != root_resolved: + raise ValueError("Path traversal detected") + return candidate diff --git a/frontend/build_frontend.ps1 b/frontend/build_frontend.ps1 new file mode 100644 index 0000000..8805e52 --- /dev/null +++ b/frontend/build_frontend.ps1 @@ -0,0 +1,65 @@ +# Ensure we are in the frontend directory +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $ScriptDir + +# $ProjectRoot = Split-Path -Parent $ScriptDir +# $BackendBuildScript = Join-Path $ProjectRoot "backend\build_backend.ps1" + +# Write-Host "Starting Full Build Process..." -ForegroundColor Cyan + +# # 1. Build Backend +# Write-Host "Invoking Backend Build..." -ForegroundColor Yellow +# & $BackendBuildScript +# if ($LASTEXITCODE -ne 0) { +# Write-Error "Backend build failed. Aborting." +# exit 1 +# } + +# 2. Build Frontend (Electron) +Write-Host "`nBuilding Frontend (Electron)..." -ForegroundColor Yellow + +# Ensure we are back in frontend directory (Backend script changes location) +Set-Location $ScriptDir + +try { + Get-Process -Name "SmartEDT" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue +} catch {} + +# Clean release directory to prevent file lock issues +if (Test-Path "release") { + Write-Host "Cleaning release directory..." -ForegroundColor Yellow + + # Try multiple times to remove the directory + $maxRetries = 3 + $retryCount = 0 + while ($retryCount -lt $maxRetries) { + try { + Remove-Item -Recurse -Force "release" -ErrorAction Stop + break + } catch { + Write-Host "Failed to clean release directory. Retrying in 2 seconds... ($($retryCount + 1)/$maxRetries)" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + try { + Get-Process -Name "SmartEDT" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + } catch {} + $retryCount++ + } + } + + if (Test-Path "release") { + Write-Warning "Could not fully clean release directory. Attempting to proceed..." + } else { + # Wait a bit to ensure filesystem is ready + Start-Sleep -Seconds 1 + } +} + +npm run dist + +if ($LASTEXITCODE -eq 0) { + Write-Host "`nFull build successful!" -ForegroundColor Green + Write-Host "Installer located at: $(Join-Path $ScriptDir 'release\SmartEDT Setup 0.1.0.exe')" -ForegroundColor Green +} else { + Write-Error "`nFrontend build failed!" + exit 1 +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a15fbfb --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + SmartEDT + + +
+ + + + diff --git a/frontend/main/main.cjs b/frontend/main/main.cjs new file mode 100644 index 0000000..40901d4 --- /dev/null +++ b/frontend/main/main.cjs @@ -0,0 +1,308 @@ +const { app, BrowserWindow, screen, Menu } = require("electron"); +const { spawn } = require("child_process"); +const path = require("path"); +const fs = require("fs"); +const os = require("os"); +const http = require("http"); + +// 简单的日志记录函数 +function log(message) { + const logPath = path.join(app.getPath("userData"), "smartedt.log"); + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + try { + fs.appendFileSync(logPath, logMessage); + } catch (e) { + console.error("Failed to write log:", e); + } +} + +let backendProcess = null; +let staticServer = null; +let backendLogStream = null; + +function resolveRepoRoot() { + return path.resolve(__dirname, "..", ".."); +} + +function getPythonPath(repoRoot) { + if (process.env.SMARTEDT_PYTHON) { + return process.env.SMARTEDT_PYTHON; + } + // Check for venv in backend (Windows) + const venvPythonWin = path.join(repoRoot, "backend", "venv", "Scripts", "python.exe"); + if (fs.existsSync(venvPythonWin)) { + return venvPythonWin; + } + // Check for venv in backend (Linux/Mac) + const venvPythonUnix = path.join(repoRoot, "backend", "venv", "bin", "python"); + if (fs.existsSync(venvPythonUnix)) { + return venvPythonUnix; + } + // Fallback to system python + return "python"; +} + +function startBackend() { + log("Starting backend process..."); + if (backendLogStream) { + try { + backendLogStream.end(); + } catch (_) {} + backendLogStream = null; + } + + const repoRoot = resolveRepoRoot(); + let backendCmd; + let args; + let cwd; + + const safeTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const backendLogPath = path.join(app.getPath("userData"), `backend-${safeTimestamp}.log`); + try { + backendLogStream = fs.createWriteStream(backendLogPath, { flags: "a" }); + backendLogStream.write(`[${new Date().toISOString()}] Backend log start\n`); + log(`Backend log file: ${backendLogPath}`); + } catch (e) { + backendLogStream = null; + log(`Failed to open backend log file: ${e.message}`); + } + + if (app.isPackaged) { + // In production, the backend is in resources/backend + const backendDir = path.join(process.resourcesPath, "backend"); + backendCmd = path.join(backendDir, "smartedt_backend.exe"); + args = ["--host", "127.0.0.1", "--port", "5000"]; + cwd = backendDir; + log(`Production mode. Backend cmd: ${backendCmd}`); + log(`Backend cwd: ${cwd}`); + } else { + // In development + const backendMain = path.join(repoRoot, "backend", "main.py"); + backendCmd = getPythonPath(repoRoot); + args = [backendMain, "--host", "127.0.0.1", "--port", "5000"]; + cwd = repoRoot; + log(`Development mode. Backend cmd: ${backendCmd}`); + } + + // 设置 PYTHONPATH 环境变量,确保能找到 backend 模块 (only for dev) + const env = { ...process.env }; + if (!app.isPackaged) { + env.PYTHONPATH = repoRoot; + } + + try { + if (!fs.existsSync(backendCmd) && app.isPackaged) { + log(`ERROR: Backend executable not found at ${backendCmd}`); + } + + backendProcess = spawn(backendCmd, args, { + cwd: cwd, + env: env, + stdio: "pipe", // Capture stdio + windowsHide: true + }); + + backendProcess.stdout.on("data", (data) => { + const text = data.toString(); + log(`[Backend stdout] ${text.trim()}`); + if (backendLogStream) { + backendLogStream.write(text.endsWith("\n") ? text : `${text}\n`); + } + }); + + backendProcess.stderr.on("data", (data) => { + const text = data.toString(); + log(`[Backend stderr] ${text.trim()}`); + if (backendLogStream) { + backendLogStream.write(text.endsWith("\n") ? text : `${text}\n`); + } + }); + + backendProcess.on("error", (err) => { + log(`Backend failed to start: ${err.message}`); + if (backendLogStream) { + backendLogStream.write(`[${new Date().toISOString()}] Backend failed to start: ${err.message}\n`); + } + }); + + backendProcess.on("exit", (code) => { + log(`Backend exited with code ${code}`); + backendProcess = null; + if (backendLogStream) { + backendLogStream.write(`[${new Date().toISOString()}] Backend exited with code ${code}\n`); + try { + backendLogStream.end(); + } catch (_) {} + backendLogStream = null; + } + }); + } catch (e) { + log(`Exception starting backend: ${e.message}`); + if (backendLogStream) { + backendLogStream.write(`[${new Date().toISOString()}] Exception starting backend: ${e.message}\n`); + try { + backendLogStream.end(); + } catch (_) {} + backendLogStream = null; + } + } +} + +function getMimeType(filePath) { + const ext = path.extname(filePath).toLowerCase(); + const mimeTypes = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff': 'application/font-woff', + '.woff2': 'font/woff2', + '.ttf': 'application/font-ttf' + }; + return mimeTypes[ext] || 'application/octet-stream'; +} + +function startLocalServer(callback) { + const distPath = path.join(__dirname, "..", "dist"); + log(`Starting local static server serving: ${distPath}`); + + staticServer = http.createServer((req, res) => { + try { + const url = new URL(req.url, `http://localhost`); + let filePath = path.join(distPath, url.pathname); + + // Security check + if (!filePath.startsWith(distPath)) { + res.statusCode = 403; + res.end('Forbidden'); + return; + } + + // Default to index.html for directories + if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { + filePath = path.join(filePath, 'index.html'); + } + + // SPA Fallback: if file not found and no extension, serve index.html + if (!fs.existsSync(filePath)) { + if (path.extname(filePath) === '') { + filePath = path.join(distPath, 'index.html'); + } else { + res.statusCode = 404; + res.end('Not Found'); + return; + } + } + + const data = fs.readFileSync(filePath); + const mimeType = getMimeType(filePath); + res.setHeader('Content-Type', mimeType); + res.end(data); + } catch (err) { + log(`Server error: ${err.message}`); + res.statusCode = 500; + res.end(`Internal Server Error`); + } + }); + + // Listen on a random available port + staticServer.listen(0, '127.0.0.1', () => { + const port = staticServer.address().port; + const url = `http://127.0.0.1:${port}`; + log(`Local static server running at ${url}`); + callback(url); + }); + + staticServer.on('error', (err) => { + log(`Static server error: ${err.message}`); + }); +} + +function createWindowForDisplay(display, routePath, baseUrl) { + const bounds = display.bounds; + const win = new BrowserWindow({ + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + autoHideMenuBar: true, + webPreferences: { + contextIsolation: true + } + }); + + win.setMenuBarVisibility(false); + if (process.platform !== "darwin") { + win.removeMenu(); + } + + // Combine baseUrl with route hash + // e.g. http://127.0.0.1:5173/#/control/config + // or http://127.0.0.1:xxxxx/#/control/config + const fullUrl = `${baseUrl}/#${routePath}`; + + log(`Loading window URL: ${fullUrl}`); + win.loadURL(fullUrl).catch(e => { + log(`Failed to load URL ${fullUrl}: ${e.message}`); + }); + + return win; +} + +function createWindows(baseUrl) { + log(`Creating windows with base URL: ${baseUrl}`); + const displays = screen.getAllDisplays(); + const primary = screen.getPrimaryDisplay(); + const others = displays.filter((d) => d.id !== primary.id); + + createWindowForDisplay(primary, "/control/config", baseUrl); + + if (others[0]) { + createWindowForDisplay(others[0], "/big/dashboard", baseUrl); + } + if (others[1]) { + createWindowForDisplay(others[1], "/car/sim", baseUrl); + } +} + +app.whenReady().then(() => { + log("App ready"); + if (process.platform !== "darwin") { + Menu.setApplicationMenu(null); + } + startBackend(); + + if (app.isPackaged) { + // Production: Start local static server + startLocalServer((serverUrl) => { + createWindows(serverUrl); + }); + } else { + // Development: Use Vite dev server + const devServerUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:5173"; + createWindows(devServerUrl); + } +}); + +app.on("before-quit", () => { + if (backendProcess) { + backendProcess.kill(); + backendProcess = null; + } + if (backendLogStream) { + try { + backendLogStream.end(); + } catch (_) {} + backendLogStream = null; + } + if (staticServer) { + staticServer.close(); + staticServer = null; + } +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..4f808dc --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5699 @@ +{ + "name": "smartedt-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smartedt-frontend", + "version": "0.1.0", + "dependencies": { + "pinia": "^3.0.4", + "uplot": "^1.6.32", + "vue": "^3.5.0", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.0", + "concurrently": "^9.0.0", + "cross-env": "^10.1.0", + "electron": "^27.3.11", + "electron-builder": "^24.13.3", + "electron-packager": "^17.1.2", + "vite": "^5.4.0", + "wait-on": "^7.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "vue": "3.5.26" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.26", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "24.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "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/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "27.3.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-27.3.11.tgz", + "integrity": "sha512-E1SiyEoI8iW5LW/MigCr7tJuQe7+0105UjqY7FkmCD12e2O6vtUbQ0j05HaBh2YgvkcEVgvQ2A8suIq5b5m6Gw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz", + "integrity": "sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==", + "deprecated": "Please use @electron/packager moving forward. There is no API change, just a package name change", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@electron/asar": "^3.2.1", + "@electron/get": "^2.0.0", + "@electron/notarize": "^1.2.3", + "@electron/osx-sign": "^1.0.5", + "@electron/universal": "^1.3.2", + "cross-spawn-windows-exe": "^1.2.0", + "debug": "^4.0.1", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^11.1.0", + "galactus": "^1.0.0", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "rcedit": "^3.0.1", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 14.17.5" + }, + "funding": { + "url": "https://github.com/electron/electron-packager?sponsor=1" + } + }, + "node_modules/electron-packager/node_modules/@electron/notarize": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz", + "integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/electron-packager/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-packager/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "7.0.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flora-colossus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", + "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/flora-colossus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/flora-colossus/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/flora-colossus/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/galactus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", + "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "flora-colossus": "^2.0.0", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/galactus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/galactus/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/galactus/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/plist": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rcedit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.1.0.tgz", + "integrity": "sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/roarr": { + "version": "2.15.4", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/rollup/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/rxjs": { + "version": "7.8.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "peer": true, + "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/temp-file": { + "version": "3.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uplot": { + "version": "1.6.32", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz", + "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/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/vue": { + "version": "3.5.26", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "license": "MIT" + }, + "node_modules/wait-on": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d2803fe --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,60 @@ +{ + "name": "smartedt-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "main/main.cjs", + "scripts": { + "dev": "concurrently -k \"vite\" \"wait-on http://127.0.0.1:5173 && cross-env ELECTRON_OVERRIDE_DIST_PATH=d:\\electron-v39.2.7-win32-x64 electron .\"", + "build": "vite build", + "preview": "vite preview", + "pack": "npm run build && electron-builder --dir", + "dist": "npm run build && electron-builder" + }, + "dependencies": { + "pinia": "^3.0.4", + "uplot": "^1.6.32", + "vue": "^3.5.0", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.0", + "concurrently": "^9.0.0", + "cross-env": "^10.1.0", + "electron": "^27.3.11", + "electron-builder": "^24.13.3", + "electron-packager": "^17.1.2", + "vite": "^5.4.0", + "wait-on": "^7.2.0" + }, + "build": { + "appId": "com.smartedt.app", + "productName": "SmartEDT", + "directories": { + "output": "release" + }, + "win": { + "target": [ + "nsis" + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + }, + "files": [ + "dist/**", + "main/**", + "package.json" + ], + "extraResources": [ + { + "from": "../backend/dist/smartedt_backend", + "to": "backend", + "filter": [ + "**/*" + ] + } + ] + } +} diff --git a/frontend/query b/frontend/query new file mode 100644 index 0000000..1a0aea4 --- /dev/null +++ b/frontend/query @@ -0,0 +1 @@ +postgresql-x64-17 diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..d93135f --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,81 @@ + + + + + + diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 0000000..9b85e1b --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,26 @@ +export const API_BASE = "http://127.0.0.1:5000"; + +export async function getHealth() { + const res = await fetch(`${API_BASE}/health`); + if (!res.ok) throw new Error(`health failed: ${res.status}`); + return res.json(); +} + +export async function startSimulation(payload) { + const res = await fetch(`${API_BASE}/api/simulation/start`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload || {}) + }); + if (!res.ok) throw new Error(`start failed: ${res.status}`); + return res.json(); +} + +export async function stopSimulation(simulationId) { + const res = await fetch(`${API_BASE}/api/simulation/${encodeURIComponent(simulationId)}/stop`, { + method: "POST" + }); + if (!res.ok) throw new Error(`stop failed: ${res.status}`); + return res.json(); +} + diff --git a/frontend/src/lib/ws.js b/frontend/src/lib/ws.js new file mode 100644 index 0000000..4fc60f7 --- /dev/null +++ b/frontend/src/lib/ws.js @@ -0,0 +1,15 @@ +export function connectWs(onMessage) { + const ws = new WebSocket("ws://127.0.0.1:5000/ws"); + ws.onmessage = (evt) => { + try { + onMessage(JSON.parse(evt.data)); + } catch { + onMessage({ type: "raw", payload: evt.data }); + } + }; + ws.onopen = () => { + ws.send("hello"); + }; + return ws; +} + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..4e9ac0a --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,36 @@ +import { createApp } from "vue"; +import { createPinia } from "pinia"; +import { createRouter, createWebHashHistory } from "vue-router"; +import App from "./App.vue"; +import BigDashboard from "./pages/BigDashboard.vue"; +import BigSystemIntro from "./pages/BigSystemIntro.vue"; +import BigSimMonitor from "./pages/BigSimMonitor.vue"; +import CarSim from "./pages/CarSim.vue"; +import ControlConfig from "./pages/ControlConfig.vue"; +import ControlOperate from "./pages/ControlOperate.vue"; +import ControlQuery from "./pages/ControlQuery.vue"; +import ControlAnalysis from "./pages/ControlAnalysis.vue"; +import ControlAdmin from "./pages/ControlAdmin.vue"; +import ServerMonitor from "./pages/ServerMonitor.vue"; + +const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { path: "/", redirect: "/control/config" }, + { path: "/big/system-intro", component: BigSystemIntro }, + { path: "/big/dashboard", component: BigDashboard }, + { path: "/big/sim-monitor", component: BigSimMonitor }, + { path: "/car/sim", component: CarSim }, + { path: "/control/config", component: ControlConfig }, + { path: "/control/operate", component: ControlOperate }, + { path: "/control/query", component: ControlQuery }, + { path: "/control/analysis", component: ControlAnalysis }, + { path: "/control/admin", component: ControlAdmin }, + { path: "/control/server-monitor", component: ServerMonitor } + ] +}); + +const pinia = createPinia(); +createApp(App).use(router).use(pinia).mount("#app"); + + diff --git a/frontend/src/pages/BigDashboard.vue b/frontend/src/pages/BigDashboard.vue new file mode 100644 index 0000000..4f6e792 --- /dev/null +++ b/frontend/src/pages/BigDashboard.vue @@ -0,0 +1,54 @@ + + + + + + diff --git a/frontend/src/pages/BigSimMonitor.vue b/frontend/src/pages/BigSimMonitor.vue new file mode 100644 index 0000000..c7c7ece --- /dev/null +++ b/frontend/src/pages/BigSimMonitor.vue @@ -0,0 +1,53 @@ + + + + + + diff --git a/frontend/src/pages/BigSystemIntro.vue b/frontend/src/pages/BigSystemIntro.vue new file mode 100644 index 0000000..0228610 --- /dev/null +++ b/frontend/src/pages/BigSystemIntro.vue @@ -0,0 +1,16 @@ + + + + + + diff --git a/frontend/src/pages/CarSim.vue b/frontend/src/pages/CarSim.vue new file mode 100644 index 0000000..9d6819b --- /dev/null +++ b/frontend/src/pages/CarSim.vue @@ -0,0 +1,36 @@ + + + + + + diff --git a/frontend/src/pages/ControlAdmin.vue b/frontend/src/pages/ControlAdmin.vue new file mode 100644 index 0000000..73347a8 --- /dev/null +++ b/frontend/src/pages/ControlAdmin.vue @@ -0,0 +1,16 @@ + + + + + + diff --git a/frontend/src/pages/ControlAnalysis.vue b/frontend/src/pages/ControlAnalysis.vue new file mode 100644 index 0000000..0e6ecb1 --- /dev/null +++ b/frontend/src/pages/ControlAnalysis.vue @@ -0,0 +1,16 @@ + + + + + + diff --git a/frontend/src/pages/ControlConfig.vue b/frontend/src/pages/ControlConfig.vue new file mode 100644 index 0000000..e41cc81 --- /dev/null +++ b/frontend/src/pages/ControlConfig.vue @@ -0,0 +1,76 @@ + + + + + + diff --git a/frontend/src/pages/ControlOperate.vue b/frontend/src/pages/ControlOperate.vue new file mode 100644 index 0000000..b101f61 --- /dev/null +++ b/frontend/src/pages/ControlOperate.vue @@ -0,0 +1,51 @@ + + + + + + diff --git a/frontend/src/pages/ControlQuery.vue b/frontend/src/pages/ControlQuery.vue new file mode 100644 index 0000000..a9bae63 --- /dev/null +++ b/frontend/src/pages/ControlQuery.vue @@ -0,0 +1,16 @@ + + + + + + diff --git a/frontend/src/pages/ServerMonitor.vue b/frontend/src/pages/ServerMonitor.vue new file mode 100644 index 0000000..756b2f6 --- /dev/null +++ b/frontend/src/pages/ServerMonitor.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/frontend/src/stores/simulation.js b/frontend/src/stores/simulation.js new file mode 100644 index 0000000..f797a5f --- /dev/null +++ b/frontend/src/stores/simulation.js @@ -0,0 +1,76 @@ +import { defineStore } from "pinia"; +import { ref, reactive } from "vue"; + +export const useSimulationStore = defineStore("simulation", () => { + const ws = ref(null); + const connected = ref(false); + const simulationId = ref(""); + const simulationStatus = ref("stopped"); + const latestSignal = reactive({}); + const serverMetrics = ref(null); // 新增服务器监控状态 + + function connect() { + if (ws.value) return; + + ws.value = new WebSocket("ws://127.0.0.1:5000/ws"); + + ws.value.onopen = () => { + connected.value = true; + console.log("WS connected"); + }; + + ws.value.onclose = () => { + connected.value = false; + ws.value = null; + console.log("WS disconnected"); + // Simple reconnect logic could be added here + setTimeout(connect, 3000); + }; + + ws.value.onmessage = (evt) => { + try { + const msg = JSON.parse(evt.data); + handleMessage(msg); + } catch (e) { + console.error("WS parse error", e); + } + }; + } + + function handleMessage(msg) { + if (msg.type === "vehicle.signal") { + if (msg.simulation_id) { + simulationId.value = msg.simulation_id; + } + if (msg.payload) { + Object.assign(latestSignal, msg.payload); + } + } else if (msg.type === "simulation.status") { + if (msg.simulation_id) { + simulationId.value = msg.simulation_id; + } + if (msg.payload && msg.payload.status) { + simulationStatus.value = msg.payload.status; + } + } else if (msg.type === "server.metrics") { + serverMetrics.value = msg.payload; + } + } + + function disconnect() { + if (ws.value) { + ws.value.close(); + ws.value = null; + } + } + + return { + connected, + simulationId, + simulationStatus, + latestSignal, + serverMetrics, + connect, + disconnect + }; +}); diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..06dbc0a --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + base: "./", + plugins: [vue()], + server: { + host: "127.0.0.1", + port: 5173 + }, + build: { + outDir: "dist", + emptyOutDir: true + } +}); + diff --git a/unity3D/unity开发的代码 b/unity3D/unity开发的代码 new file mode 100644 index 0000000..7a23fe8 --- /dev/null +++ b/unity3D/unity开发的代码 @@ -0,0 +1,2 @@ +此目录放置unity开发的代码 +增了电池电量显示 \ No newline at end of file diff --git a/智能电动车数字孪生系统功能规划2026.pdf b/智能电动车数字孪生系统功能规划2026.pdf new file mode 100644 index 0000000..fa47fbe Binary files /dev/null and b/智能电动车数字孪生系统功能规划2026.pdf differ diff --git a/系统开发技术方案.md b/系统开发技术方案.md new file mode 100644 index 0000000..beefe9b --- /dev/null +++ b/系统开发技术方案.md @@ -0,0 +1,416 @@ +# 智能电动汽车数字孪生系统开发技术方案(V1) +智能电动车数字孪生系统软件通过集成高精度数据采集、实时同步控制、三维虚拟仿真、智能算法验证与系统运维管理等功能,构建了一个覆盖教学、科研与产业应用的智能电动汽车虚实融合实验平台。它不仅能实现真实驾驶环境的沉浸式再现与数据级同步,还能为科研人员提供精准可追溯的数据支持、灵活的算法验证接口和可扩展的数据分析工具,充分满足智能电动汽车领域对实验真实性、可靠性与创新性的高标准要求,推动智能驾驶与数字孪生技术的深度融合与发展。 +本文基于《智能电动车数字孪生系统功能规划(2026)》的目标,给出一套可落地的软件开发技术方案。Unity 三维驾驶仿真作为独立工程(独立代码库与发布包),通过标准接口与本系统对接。 + + +## 1. 建设目标与原则 + +### 1.1 建设目标 +- 实现多源设备数据采集、统一管理与实时可视化展示 +- 构建“实体车—数字孪生体”数据交互:状态同步、数据采集、事件回放 +- 支撑多场景驾驶仿真联动(Unity 独立运行),实现同屏/多屏展示与协同操作 +- 实现实验管理与记录:仿真记录创建、过程记录、视频录制、数据持久化、历史回放 +- 本地化部署与数据安全:本地存储、最小暴露接口、路径越界防护 + +### 1.2 设计原则 +- 分层解耦:设备接入、业务编排、数据服务、可视化展示相互独立 +- 实时优先:以事件/流式数据驱动为核心(WebSocket + HTTP) +- 可扩展:设备类型、消息类型、算法模块、Unity 场景可插拔扩展 +- 可运维:统一配置、日志、诊断接口、打包发布与自动启动链路 + +## 2. 总体架构 + +### 2.1 架构分解 +系统由三类可独立发布、独立升级的可执行程序组成: + +1) **后端数据与控制服务(Python EXE)** +- 负责设备采集、数据处理、仿真记录管理、持久化、文件存储、对外 API +- 对前端提供 HTTP REST + WebSocket(实时推送) +- 对 Unity 提供联动接口(可选双向:状态推送/指令下发) + +2) **桌面端可视化应用(Electron + Vue EXE)** +- 负责 UI 展示、实验流程操作、多屏组织、外部程序(后端/Unity)启动与守护 +- 作为“应用入口”,统一呈现与统一运维 + +3) **车辆驾驶仿真(Unity EXE,外部项目)** +- 负责 3D 场景、动力学/视觉化仿真、互动控制 +- 与后端通过约定协议交换数据(HTTP/WebSocket/本地端口均可) + +### 2.2 数据流与控制流 +- **采集流**:设备/传感器 → 设备接入层 → 统一事件总线 → 处理/校准 → 入库/落盘 → WebSocket 推送 +- **控制流**:前端操作 → REST 指令 → 业务编排 → 下发到设备/控制柜,或同步给 Unity +- **回放流**:历史仿真记录 → 查询元数据(数据库)→ 拉取文件(静态映射)→ 前端播放/曲线回放,或驱动 Unity 回放 + +### 2.3 系统两大部分划分 +系统整体可分为两大部分(对应团队分工与发布包边界): + +1) **数据采集与处理(后端)** +- 采集:方向盘/踏板/档位/手刹/车速轮速/灯光/电参/温度等 +- 处理:滤波、校准、融合、异常检测、指标计算 +- 管理:仿真记录、文件、数据库、权限(可选) +- 服务:REST + WebSocket + 静态文件映射 + +2) **界面显示与交互(Electron + Vue)** +- 显示大屏:系统介绍、仿真监控、数据看板 +- 车载屏(驾驶员):驾驶视角信息与仪表盘 +- 控制屏:场景配置、系统配置、用户/权限、数据查询、数据分析 + +### 2.4 推荐工程目录结构(本仓库) +建议把“后端服务”和“前端桌面端”放在同一仓库便于协同开发;Unity 工程保持外部独立仓库,仅提交接口文档与发布包获取方式。 + +``` +SmartEDT/ + backend/ # Python:采集/处理/管理/API + device/ # 传感器设备接入与驱动(方向盘/踏板/灯光/电参等) + database/ # 数据存储(PostgreSQL/TimescaleDB)与数据访问层 + config/ # 配置(ini/环境变量)与配置加载 + main.py # 后端入口(启动 API/WS、初始化各模块) + utils.py # 通用工具(时间/路径/校验/日志等) + frontend/ # Electron + Vue:桌面端与多屏展示 + src/ # Vue 渲染进程(页面/组件/路由) + main/ # Electron 主进程(多窗口、多屏、拉起后端/Unity) + (配置文件) # package.json / vite.config.* / electron-builder 配置等 + docs/ # 方案、接口协议、运维文档 + data/ # 默认数据目录(开发态可用) + release/ # 可选:打包产物临时输出目录(不提交版本库) +``` + +## 3. 技术选型与理由 + +### 3.1 后端(Python) +- **语言**:Python 3.13.1(配合硬件 SDK、算法生态与快速迭代) +- **Web 框架**:FastAPI(异步友好、类型约束清晰、OpenAPI 自动生成) +- **实时通讯**:WebSocket(FastAPI 原生或 Starlette WebSocket),用于状态/传感数据/事件推送 +- **进程内并发**:asyncio + 线程池/进程池(适配阻塞型硬件 SDK) +- **持久化**:PostgreSQL18 + TimescaleDB(用于实时采集数据的时序存储)+ SQLAlchemy(ORM/迁移支持) +- **文件存储**:本地文件系统(与数据库元数据关联) +- **打包**:PyInstaller 打包为独立 EXE;开发环境使用 venv 隔离依赖 + + +### 3.2 前端(Electron + Vue) +- **UI 框架**:Vue 3 + Vite(开发体验与生态成熟) +- **桌面封装**:Electron(多窗口、多屏支持、可管理外部进程) +- **本地通信**:HTTP + WebSocket 访问后端;可选 IPC(主进程与渲染进程) +- **打包**:electron-builder 产出 Windows EXE 安装包/绿色包 + +### 3.3 Unity(外部) +- **三维仿真**:Unity(高效构建交互式驾驶场景与渲染) +- **对接方式**:建议 WebSocket(低延迟实时同步)+ HTTP(配置、仿真记录、资源拉取) +- **进程管理**:由 Electron 主进程统一拉起与关闭(可在指定显示器全屏) + +## 4. 后端模块设计(Python) + +### 4.1 模块划分 +- **配置中心(Config)** + - 统一读取 `config.ini`(就近优先原则:后端目录优先,其次项目根目录) + - 管理文件根目录、数据库路径、端口、日志等级等 +- **设备接入层(Adapters/Drivers)** + - 每类设备一个管理器:相机、IMU、压力/力传感、控制柜等 + - 统一生命周期:init/connect/start/stop/disconnect + - 统一数据模型:采集原始数据 + 时间戳 + 设备元信息 +- **设备协调器(Coordinator)** + - 负责多设备协同启动/停止、状态聚合、异常重连策略 + - 对上提供“仿真记录级操作”,对下调度各设备管理器 +- **数据处理层(Pipeline)** + - 校准/滤波/坐标变换/融合(按需求插拔) + - 统一输出“标准事件”,写入事件总线与持久化队列 +- **仿真实验管理(Simulation Experiment)** + - 流程:场景配置 → 仿真启动 → 仿真执行 → 仿真结束 +- **数据存储(Storage)** + - 元数据:PostgreSQL(仿真记录、设备、事件索引、文件索引) + - 实时采集数据:TimescaleDB Hypertable(高频写入、按时间范围查询与聚合) + - 典型能力:按时间分区、压缩、保留策略、连续聚合(按需要启用) + - 大文件:视频、原始采样、导出报告等存本地文件系统 +- **录制与归档(Recording/Archiving)** + - 仿真过程视频录制:显示大屏/车载屏/Unity 画面按需求录制(目标帧率 30fps) + - 录制任务与仿真记录绑定:开始/停止随仿真流程联动,支持异常中止与补偿关闭 + - 文件索引入库:形成可回放、可导出的统一入口 +- **对外服务(API)** + - REST:健康检查、设备管理、仿真记录控制、数据查询、导出 + - WebSocket:实时推送(状态、传感数据、告警、进度) +- **静态文件映射(File Gateway)** + - 按配置的根目录对外只读映射 + - 路径规范化、拒绝越界(拦截 `..`、绝对路径、UNC 路径) + +### 4.2 采集数据范围与精度(本期基线) +本系统“采集与处理”侧的基线采集字段如下(用于实时显示、联动仿真与历史回放);后续可按车型/传感器扩展。 + +- **转向与踏板** + - 方向盘转角:左/右(单位建议:deg) + - 刹车踏板行程:mm + - 电门踏板行程:mm +- **车辆状态** + - 挡位:P / N / D / S + - 手刹状态(手状态):0 / 1 + - 车速:单位建议 km/h + - 车轮转速:单位建议 rpm(按轮位可扩展为 FL/FR/RL/RR) +- **灯光状态** + - 左转向灯 / 右转向灯 + - 双闪 + - 刹车灯 +- **电池与电参** + - 电池剩余电量:% + - 电压:测量误差 0.1V + - 电流:测量误差 0.1A + - 温度:测量误差 0.5℃ + +采集来源建议按两类接入: +- **传感器/控制柜直采**:方向盘、踏板、档位、手刹、车速/轮速、电参等(串口/CAN/以太网,取决于硬件) +- **视觉识别采集(可选)**:当部分状态仅存在于仪表或屏幕显示时,用相机 + 识别推断(需标注数据与容错策略) + + +### 4.3 API 设计建议(摘要) +- `GET /health`:健康检查(版本、时间、依赖状态) +- `GET /api/devices`:设备列表与状态 +- `POST /api/simulation/start`:创建并启动一次三维仿真记录(返回 simulation_id) +- `POST /api/simulation/{simulation_id}/stop`:结束一次三维仿真并落盘 +- `GET /api/simulation/{simulation_id}`:三维仿真记录元数据 +- `GET /api/simulation/{simulation_id}/events`:事件/曲线数据查询(分页/时间窗) +- `GET /files/`:静态文件访问映射(对标 README 的安全校验策略) +- `WS /ws`:统一实时通道(按 topic/room 订阅) + +### 4.4 实时消息模型(建议) +采用统一 JSON Envelope,便于前端与 Unity 复用: + +```json +{ + "type": "vehicle.signal", + "ts": 1737000000.123, + "simulation_id": "SIM202601160001", + "device_id": "controlbox_01", + "seq": 1024, + "payload": { + "steering_wheel_angle_deg": 12.3, + "brake_pedal_travel_mm": 5.2, + "throttle_pedal_travel_mm": 18.0, + "gear": "D", + "handbrake": 0, + "vehicle_speed_kmh": 36.5, + "wheel_speed_rpm": { "FL": 320, "FR": 319, "RL": 318, "RR": 318 }, + "lights": { "left_turn": 0, "right_turn": 1, "hazard": 0, "brake": 0 }, + "soc_percent": 78.2, + "voltage_v": 356.4, + "current_a": 12.7, + "temperature_c": 28.5 + } +} +``` + +建议补充字段: +- `source`:device/backend/unity +- `quality`:丢包/校准状态/信噪比等 +- `schema_version`:前后端与 Unity 协同升级 + +### 4.5 目录与文件存储规范(建议) +与 README 的建议一致,采用“根目录 + 仿真记录分层”的稳定结构: + +``` +/ + / + / + meta.json + video/ + signals/ + exports/ +``` + +文件根目录通过 `[FILEPATH].path` 配置;当配置为相对路径时,开发环境相对后端工作目录,打包环境相对后端 EXE 同级目录(对标 README 的路径策略)。 + +存储类型建议(与规划大纲一致): +- 传感器原始数据:生成数据文件(如 `signals/raw_*.dat`),按时间段或按设备分文件落盘 +- 传感器/识别后的结构化数据:实时写入 PostgreSQL/TimescaleDB(用于查询、统计、回放曲线) +- 仿真记录视频:按窗口/视角分别落入 `video/`,并在数据库中建立索引与标签(视角、分辨率、帧率等) + +### 4.6 TimescaleDB 时序数据建模建议 +为支持“高频写入 + 按时间范围回放/聚合”,建议将采集数据按时序表落库,并在 TimescaleDB 中转换为 Hypertable。 + +推荐思路: +- 一张时序主表:按 `ts` 做 Hypertable 分区 +- 以 `simulation_id` 作为业务主维度,支持一次三维仿真记录的时间窗查询与回放 + +字段建议(可按性能需要做列化/半结构化折中): +- `ts`:时间戳(TimescaleDB 分区键) +- `simulation_id`:三维仿真记录 ID(用于回放与归档) +- `device_id`:数据来源设备 +- `signals`:结构化信号(可选 JSONB,或将高频字段拆为列以提升聚合效率) + +典型策略(按需启用): +- 保留策略:只保留近 N 天原始点,长期保留用压缩或降采样结果 +- 连续聚合:生成 1s/100ms 粒度的统计序列用于大屏曲线与回放加速 + +## 5. 前端模块设计(Electron + Vue) + +### 5.1 界面体系(显示大屏 / 车载屏 / 控制屏) +界面显示功能分为三类屏幕(与规划大纲一致),由 Electron 统一创建窗口并在指定显示器呈现,Vue 负责页面实现。 + +1) **显示大屏(面向展示/观摩)** +- **系统介绍** + - 系统功能介绍:图文并茂;可扩展语音播报与 AI 问答导览 + - 车辆三维展示:外观/内饰动态展示;关键部件结构展示(转向机构、驱动机构等) + - 系统原理展示:动力/转向/制动/灯光等子系统工作原理可视化 +- **数据看板** + - 数据主屏:中间为车辆三维动态模型;周边展示车辆概况、车辆参数(示例:秦 Plus)、历次仿真记录、设备监控(CPU/内存)、模拟报警信息 + - 仿真回放屏:对单次仿真结果回放;三维仿真回放 + 采集数据动态图表回放;进度显示(总时长/当前进度) +- **仿真监控** + - 视角切换:主视角 / 驾驶员视角 + - 主视角:路线图(动态)+ 车辆尾随视角 + - 驾驶员视角:三维场景动态 + 仪表盘(车速、转速、档位、手刹、车灯、电量等) + - 仿真视频录制:动态录制仿真视频过程屏幕(目标 30fps),并与仿真记录绑定 + +2) **车载屏(驾驶员视角)** +- 仿真界面:三维仿真场景;天气与时段显示;车速显示;仿真状态标识(等待/仿真中/结束) +- 数据记录:后台记录仿真过程记录,并动态录制车载屏仿真屏幕(目标 30fps) + +3) **控制屏(操作与管理)** +- 场景配置 + - 仿真场景:城市道路/高速公路/山区道路/乡村道路/校园道路 + - 气象:晴/雨/雪/雾等;时段:黎明/正午/黄昏/夜间 + - 车辆颜色:红/白/黑;最高限速:100–180 km/h + - 仿真时长:5–60 分钟;驾驶员选择:人员/账号绑定 + - 车辆传感器校正:自检/校正 + - 场景推送:下发到车载屏与显示大屏 +- 仿真控制:开始仿真 / 结束仿真 +- 系统配置:功能介绍(文字)、端口与路径等基础配置入口 +- 用户配置:角色与权限配置;用户与角色选择 +- 数据查询:系统日志查询;仿真记录查询 +- 数据分析:车辆数据分析、故障模拟诊断、CAN 总线数据模拟解析 + +### 5.2 与后端的通信 +- REST:用于仿真记录控制、配置读写、历史查询、导出触发 +- WebSocket:订阅实时 topic(设备状态、采样数据、告警、录制进度) +- 静态文件:通过后端安全映射路径访问视频与大文件(支持 Range) + +### 5.3 前端路由与窗口映射(建议) +为便于多屏稳定运行,建议把“窗口类型”与“路由入口”固化: +- 显示大屏 + - `/big/system-intro` + - `/big/dashboard` + - `/big/sim-monitor` +- 车载屏 + - `/car/sim` +- 控制屏 + - `/control/config` + - `/control/operate` + - `/control/query` + - `/control/analysis` + - `/control/admin` + +### 5.4 外部进程启动与守护(Electron 主进程) +主进程负责统一拉起与管理: +- 后端 EXE:启动时分配端口(固定端口或自动探测),轮询 `/health` 确认就绪 +- Unity EXE:按用户选择显示器启动(命令行传参:端口、simulation_id、模式等) +- 异常策略:崩溃检测、重启次数限制、日志采集、用户提示 + +推荐机制: +- 端口管理:优先读配置,冲突时自动寻找可用端口并回写运行态配置 +- 生命周期:Electron 退出时优雅关闭后端与 Unity(超时后强制) + +## 6. Unity 联动方案(外部项目对接约定) + +### 6.1 对接目标 +- 实时同步实体车/传感器状态到 Unity(姿态、速度、踏板、方向、报警等) +- Unity 交互事件回传(场景切换、碰撞事件、驾驶行为、训练步骤等) +- 支持“实时模式”和“回放模式” + +### 6.2 建议协议 +建议使用同一套消息 Envelope: +- **WebSocket(实时)**:订阅 `vehicle.signal`、`control.command` 等;Unity 回传 `unity.event.*` +- **HTTP(控制/查询)**:加载仿真记录、读取配置、拉取资源、触发导出 + +### 6.3 同步与时钟策略 +- 后端为权威时间源(服务端时间戳),Unity 按 `ts` 对齐插值 +- 允许 Unity 维护环形缓冲(例如 100–300ms)平衡抖动与延迟 + +## 7. 配置与环境管理 + +### 7.1 venv 管理与依赖 +- 开发环境:`python -m venv venv`,使用 `requirements.txt` 固定依赖版本 +- 构建环境:单独 `requirements_build.txt`(包含 PyInstaller 等打包依赖) + +### 7.2 配置文件(INI) +建议统一使用以下段落(可按需扩展): + +```ini +[SERVER] +host = 0.0.0.0 +port = 5000 + +[FILEPATH] +path = data + +[DATABASE] +url = postgresql+psycopg://smartedt:CHANGE_ME@127.0.0.1:5432/smartedt +timescaledb = True +``` + +## 8. 打包发布与部署 + +### 8.1 后端打包(PyInstaller) +产物目标: +- `Backend.exe`(单文件或目录模式) +- `config.ini`(可外置,便于现场修改) +- `data/`(默认数据目录,可按配置指向其他盘符) + +建议输出: +- 内置 OpenAPI 文档(仅本机或受限网络可访问) +- 日志写入到 `logs/`(按日期切分) + +### 8.2 前端打包(Electron) +产物目标: +- `SmartEDT.exe`(安装包/绿色包) +- `resources/backend/Backend.exe`(随包携带或首次运行下载) +- 统一版本号与升级策略(可选增量更新) + +### 8.3 一键启动链路 +1. 启动 `SmartEDT.exe` +2. Electron 主进程读取配置并拉起 `Backend.exe` +3. 后端健康检查通过后,渲染进程连接 REST/WS +4. 用户选择“仿真模式”时,拉起 Unity EXE 并注入运行参数 + +## 9. 多屏显示方案 + +### 9.1 目标 +- 控制屏在主屏(操作与管理入口) +- 显示大屏在副屏/大屏(系统介绍 / 数据看板 / 仿真监控) +- 车载屏在驾驶员屏(或指定小屏) +- Unity 全屏运行在指定显示器(可与显示大屏分离) + +### 9.2 Electron 实现要点 +- 启动时枚举显示器,建立“显示器—窗口类型”映射 +- 针对不同显示器创建多个 BrowserWindow(固定 URL/路由) +- 提供“显示器管理”设置页:拖拽绑定、保存配置、开机恢复 +- Unity 启动参数附带目标显示器索引(或由 Unity 自行选择) + +建议的窗口类型: +- BigScreenWindow:显示大屏(可切换 system-intro/dashboard/sim-monitor) +- CarScreenWindow:车载屏(驾驶员视角) +- ControlWindow:控制屏(配置、控制、查询、分析、管理) + +## 10. 安全、可靠性与合规 + +### 10.1 本地数据安全 +- 默认仅本机访问(可通过配置开放局域网) +- 静态文件映射做路径规范化与越界拦截(对标 README 的安全策略) +- 敏感字段脱敏显示(subject_id 映射/匿名化) + +### 10.2 稳定性策略 +- 设备断连:自动重连、指数退避、状态广播 +- 写盘保护:异步队列 + 批量提交;异常时保证仿真记录可关闭并可恢复 +- 资源管理:视频录制与采样队列限速,避免磁盘/CPU 被打满 + +## 11. 日志、诊断与监控 +- 分级日志:设备/仿真记录/API/存储 +- 诊断接口:`/health` 返回依赖与核心子系统状态(设备数量、队列长度等) +- 现场排障:一键导出日志与仿真记录元数据(不含隐私或做脱敏) + +## 12. 测试与质量保证 +- 单元测试:消息模型、路径安全、数据库 CRUD、仿真流程状态机 +- 集成测试:模拟设备数据源(Mock Adapter),验证 WS 推送与落盘 +- 性能测试:高频 IMU、视频录制并发、长时仿真稳定性(内存/磁盘增长) + +## 13. 交付物清单(建议) +- 后端:可执行程序、配置模板、API 文档、设备接入开发规范 +- 前端:桌面 EXE、显示器管理、实验流程 UI、日志导出 +- 对接:Unity 通讯协议文档(topic/消息体/端口/参数)、联调用例 +- 运维:安装说明、目录与数据管理说明、故障排查手册 + diff --git a/系统框架结构评估.md b/系统框架结构评估.md new file mode 100644 index 0000000..5a36f31 --- /dev/null +++ b/系统框架结构评估.md @@ -0,0 +1,66 @@ +# 系统框架结构评估与优化建议 + +本文档基于 `d:\Trae_space\SmartEDT\README.md` 与实际代码结构,对已生成的 SmartEDT 系统框架(后端 Python 3.13.1 + FastAPI + TimescaleDB,前端 Electron + Vue)进行评估与分析,并提出优化改进点。 + +## 1. 总体评估 + +**结论**:系统框架整体架构合理,层次清晰,符合《系统开发技术方案.md》的要求,能够支撑后续功能开发。 + +**亮点**: +- **架构分层清晰**:后端按 `api`(接口)、`services`(业务逻辑)、`database`(存储)、`device`(硬件抽象)分层,职责单一。 +- **技术选型落地准确**:Python 3.13.1 + FastAPI + WebSocket + PostgreSQL/TimescaleDB 组合落地,前端 Electron + Vue 多窗口方案已具雏形。 +- **工程化细节完备**:包含 `config.ini` 配置加载、Mock 设备实现、Docker 友好的环境变量覆盖、静态文件安全映射、以及自动化的数据库 Schema 初始化。 +- **扩展性良好**:`SimulationManager` 封装了仿真生命周期,`MockVehicleDevice` 提供了设备接入的模板,便于后续替换为真实硬件驱动。 + +## 2. 不足与改进建议 + +尽管骨架已跑通,但在高频采集、健壮性与可维护性方面仍有优化空间: + +### 2.1 后端(Backend) + +1. **数据库连接池与异步写入优化** + - **现状**:`SimulationManager._run_loop` 中每次采集都调用 `_persist_signal`,内部使用 `async with self._session_factory()` 创建新 Session 并提交。 + - **问题**:高频采集(如 50ms/20Hz)下,频繁创建/销毁 Session 会带来性能开销;且单条 Insert 效率较低。 + - **改进**: + - 引入 **批量写入队列**(Batch Buffer),每 N 条或每 M 秒批量 `COPY` 或 `INSERT` 到 TimescaleDB。 + - 确保 `engine` 配置了合理的连接池大小(`pool_size`, `max_overflow`)。 + +2. **异常处理与优雅退出** + - **现状**:`main.py` 的 `shutdown` 钩子中只处理了 `simulation_manager.stop`。 + - **问题**:如果 WebSocket 广播队列积压或数据库连接卡死,服务可能无法优雅退出。 + - **改进**: + - 增加全局异常捕获(Global Exception Handler)。 + - 在 `Broadcaster` 中增加队列满或断连的防御性逻辑。 + +3. **配置热加载与验证** + - **现状**:`settings.py` 仅在启动时加载一次。 + - **问题**:运行时无法动态调整日志级别或采集频率。 + - **改进**:虽然不需要完全热加载,但建议增加配置项的 Pydantic 校验(当前已用 `pydantic-settings`,符合预期,但可增强校验规则)。 + +4. **设备抽象层增强** + - **现状**:`MockVehicleDevice` 直接返回字典。 + - **改进**:引入 Pydantic 模型或 TypedDict 定义设备数据协议,确保 `payload` 结构在代码层面有强类型约束。 + +### 2.2 前端(Frontend) + +1. **状态管理缺失** + - **现状**:各 `.vue` 页面独立通过 `useWebSocket` 订阅数据。 + - **问题**:多窗口间(如控制屏与大屏)可能需要共享某些全局状态(如当前仿真 ID、连接状态),各自维护连接可能造成资源浪费或状态不一致。 + - **改进**:引入 Pinia 进行全局状态管理(如 `useSimulationStore`),统一管理 WS 连接与消息分发。 + +2. **Electron 进程通信 (IPC) 封装** + - **现状**:`main.cjs` 启动了后端,但未建立健壮的 IPC 通道来透传后端日志或状态给渲染进程。 + - **改进**:增加 `preload.js`,通过 `contextBridge` 暴露后端进程状态(PID、启动日志)给控制屏前端,便于运维排查。 + +3. **多屏窗口管理增强** + - **现状**:`createWindows` 硬编码了 `others[0]`、`others[1]`。 + - **改进**:增加配置文件(`window-layout.json`)记录屏幕 ID 与窗口路由的绑定关系,实现“开机自动恢复上次布局”。 + +## 3. 优化计划 + +基于以上分析,将对代码进行以下针对性优化: + +1. **后端**:优化 `SimulationManager` 的入库逻辑,改为 `Buffer` 批量写入模式。 +2. **后端**:增强 `device` 数据模型的类型定义。 +3. **前端**:引入 Pinia 并重构 WS 逻辑为 Store 模式。 +4. **工程**:补充 `docker-compose.yml` 以便快速拉起 PostgreSQL + TimescaleDB 环境。