From 6e4c7c1050ad7c08403a3ed9afc0065f40b0fbe9 Mon Sep 17 00:00:00 2001 From: LoveChengke Date: Fri, 11 Sep 2026 22:46:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop-client):=20=E9=98=B6=E6=AE=B54-6?= =?UTF-8?q?=20Electron=20EXE=20=E5=AE=A2=E6=88=B7=E7=AB=AF=EF=BC=88?= =?UTF-8?q?=E7=99=BB=E5=BD=95/=E8=AF=BE=E8=A1=A8/=E4=BD=9C=E4=B8=9A/?= =?UTF-8?q?=E9=80=9A=E7=9F=A5/=E6=88=90=E7=BB=A9/=E8=AE=BE=E7=BD=AE=20+=20?= =?UTF-8?q?Socket.IO=20=E5=AE=9E=E6=97=B6=20+=20IndexedDB=20=E7=A6=BB?= =?UTF-8?q?=E7=BA=BF=E7=BC=93=E5=AD=98=E4=B8=8E=E8=87=AA=E5=8A=A8=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20+=20electron-builder=20=E6=89=93=E5=8C=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 262 ++- package.json | 9 +- packages/desktop-client/electron-builder.yml | 42 + packages/desktop-client/index.html | 13 + packages/desktop-client/package.json | 40 + .../desktop-client/scripts/build-main.mjs | 37 + packages/desktop-client/scripts/dev.mjs | 67 + packages/desktop-client/scripts/dist-win.mjs | 63 + packages/desktop-client/scripts/smoke.mjs | 35 + packages/desktop-client/src/main/config.ts | 110 + packages/desktop-client/src/main/index.ts | 102 + packages/desktop-client/src/main/ipc.ts | 45 + packages/desktop-client/src/main/smoke.ts | 161 ++ packages/desktop-client/src/preload/index.ts | 19 + packages/desktop-client/src/renderer/App.vue | 72 + .../desktop-client/src/renderer/api/http.ts | 95 + .../desktop-client/src/renderer/api/index.ts | 61 + .../desktop-client/src/renderer/cache/db.ts | 133 ++ .../src/renderer/cache/index.ts | 54 + .../src/renderer/cache/self-test.ts | 152 ++ .../desktop-client/src/renderer/config.ts | 26 + packages/desktop-client/src/renderer/env.d.ts | 1 + .../src/renderer/layouts/ClientLayout.vue | 238 +++ packages/desktop-client/src/renderer/main.ts | 29 + .../src/renderer/router/index.ts | 70 + .../desktop-client/src/renderer/stores/app.ts | 132 ++ .../src/renderer/stores/auth.ts | 131 ++ .../src/renderer/stores/notifications.ts | 107 + .../src/renderer/stores/realtime.ts | 148 ++ .../src/renderer/styles/index.css | 115 ++ .../src/renderer/views/GradeView.vue | 259 +++ .../src/renderer/views/HomeworkView.vue | 238 +++ .../src/renderer/views/LoginView.vue | 221 ++ .../src/renderer/views/NotificationView.vue | 228 +++ .../src/renderer/views/ScheduleView.vue | 172 ++ .../src/renderer/views/SettingsView.vue | 211 ++ .../desktop-client/src/types/desktop.d.ts | 49 + packages/desktop-client/tsconfig.json | 23 + packages/desktop-client/tsconfig.node.json | 13 + packages/desktop-client/vite.config.mts | 32 + pnpm-lock.yaml | 1809 +++++++++++++++++ pnpm-workspace.yaml | 3 + 42 files changed, 5767 insertions(+), 60 deletions(-) create mode 100644 packages/desktop-client/electron-builder.yml create mode 100644 packages/desktop-client/index.html create mode 100644 packages/desktop-client/package.json create mode 100644 packages/desktop-client/scripts/build-main.mjs create mode 100644 packages/desktop-client/scripts/dev.mjs create mode 100644 packages/desktop-client/scripts/dist-win.mjs create mode 100644 packages/desktop-client/scripts/smoke.mjs create mode 100644 packages/desktop-client/src/main/config.ts create mode 100644 packages/desktop-client/src/main/index.ts create mode 100644 packages/desktop-client/src/main/ipc.ts create mode 100644 packages/desktop-client/src/main/smoke.ts create mode 100644 packages/desktop-client/src/preload/index.ts create mode 100644 packages/desktop-client/src/renderer/App.vue create mode 100644 packages/desktop-client/src/renderer/api/http.ts create mode 100644 packages/desktop-client/src/renderer/api/index.ts create mode 100644 packages/desktop-client/src/renderer/cache/db.ts create mode 100644 packages/desktop-client/src/renderer/cache/index.ts create mode 100644 packages/desktop-client/src/renderer/cache/self-test.ts create mode 100644 packages/desktop-client/src/renderer/config.ts create mode 100644 packages/desktop-client/src/renderer/env.d.ts create mode 100644 packages/desktop-client/src/renderer/layouts/ClientLayout.vue create mode 100644 packages/desktop-client/src/renderer/main.ts create mode 100644 packages/desktop-client/src/renderer/router/index.ts create mode 100644 packages/desktop-client/src/renderer/stores/app.ts create mode 100644 packages/desktop-client/src/renderer/stores/auth.ts create mode 100644 packages/desktop-client/src/renderer/stores/notifications.ts create mode 100644 packages/desktop-client/src/renderer/stores/realtime.ts create mode 100644 packages/desktop-client/src/renderer/styles/index.css create mode 100644 packages/desktop-client/src/renderer/views/GradeView.vue create mode 100644 packages/desktop-client/src/renderer/views/HomeworkView.vue create mode 100644 packages/desktop-client/src/renderer/views/LoginView.vue create mode 100644 packages/desktop-client/src/renderer/views/NotificationView.vue create mode 100644 packages/desktop-client/src/renderer/views/ScheduleView.vue create mode 100644 packages/desktop-client/src/renderer/views/SettingsView.vue create mode 100644 packages/desktop-client/src/types/desktop.d.ts create mode 100644 packages/desktop-client/tsconfig.json create mode 100644 packages/desktop-client/tsconfig.node.json create mode 100644 packages/desktop-client/vite.config.mts diff --git a/README.md b/README.md index 028a231..5e22556 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ ## 当前交付范围 -| 阶段 | 内容 | 状态 | -| ---- | ----------------------------------------------------------------------------------------- | --------------------------------------------- | -| 1 | pnpm monorepo 脚手架、TypeScript / ESLint / Prettier / 环境变量 | ✅ 已完成 | -| 2 | 后端:Prisma schema + 迁移 + 种子数据 + JWT 认证 + RBAC + 模块化 REST API + Socket.IO | ✅ 已完成 | -| 3 | Web 管理端:登录、主布局、仪表盘、班级/学生/课表/作业/通知/成绩页面、Axios 封装、实时提示 | ✅ 已完成 | -| 4 | EXE 客户端(Electron + Vue 3 + Element Plus + Socket.IO Client + 离线缓存) | ⏳ 待开发 | -| 5 | 三端联调脚本、打包命令、完整 README | 🟡 后端联调脚本与文档已完成,EXE 打包待阶段 4 | -| 6 | 测试账号与种子数据说明 | ✅ 已完成(见下文) | +| 阶段 | 内容 | 状态 | +| ---- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| 1 | pnpm monorepo 脚手架、TypeScript / ESLint / Prettier / 环境变量 | ✅ 已完成 | +| 2 | 后端:Prisma schema + 迁移 + 种子数据 + JWT 认证 + RBAC + 模块化 REST API + Socket.IO | ✅ 已完成 | +| 3 | Web 管理端:登录、主布局、仪表盘、班级/学生/课表/作业/通知/成绩页面、Axios 封装、实时提示 | ✅ 已完成 | +| 4 | EXE 客户端:Electron 主进程/preload/渲染进程、登录、课表/作业/通知/成绩/设置、实时推送、IndexedDB 离线缓存 | ✅ 已完成(冒烟验证 9/9 通过) | +| 5 | 三端联调脚本、打包命令、完整 README | ✅ 已完成(electron-builder 打包 Windows EXE) | +| 6 | 测试账号与种子数据说明 | ✅ 已完成(见下文) | > **与原始提示词的两处偏差(已与你确认)** > @@ -36,7 +36,7 @@ ``` class-helper/ -├── package.json # 根脚本(dev / build / db:* / verify:e2e) +├── package.json # 根脚本(dev / build / db:* / verify:* / dist:*) ├── pnpm-workspace.yaml # workspace 定义 + allowBuilds(pnpm 11 依赖构建白名单) ├── tsconfig.base.json # 共享 TS 基础配置 ├── eslint.config.mjs # ESLint 扁平配置(TS + Vue) @@ -50,31 +50,42 @@ class-helper/ │ ├── prisma/migrations/ # 迁移历史(已生成并应用) │ ├── prisma/seed.ts # 种子数据 │ ├── prisma.config.ts # Prisma 7 配置(迁移/种子/连接串) - │ ├── scripts/verify-e2e.mjs # 端到端验收脚本(54 项) + │ ├── scripts/verify-e2e.mjs # 后端端到端验收脚本(54 项) │ └── src/ - │ ├── app.ts # Express 装配(健康检查 + 模块挂载) + │ ├── app.ts # Express 装配(服务首页 + 健康检查 + 模块挂载) │ ├── index.ts # 启动入口(HTTP + Socket.IO + 优雅退出) │ ├── config/env.ts # 环境变量校验(zod) │ ├── lib/ # db / http / jwt / password / access(RBAC) / mappers │ ├── middleware/ # auth / error / validate │ ├── realtime/ # socket.ts + bus.ts(事件总线) │ └── modules/ # 功能模块 + registry.ts - └── web-admin/ # Web 管理端(Vue 3 + Vite + Element Plus) - └── src/{api,stores,router,layouts,views,styles} + ├── web-admin/ # Web 管理端(Vue 3 + Vite + Element Plus) + │ └── src/{api,stores,router,layouts,views,styles} + └── desktop-client/ # EXE 客户端(Electron + Vue 3) + ├── electron-builder.yml # 打包配置(nsis 安装包 + portable 单文件) + ├── vite.config.mts # 渲染进程构建(base: './',hash 路由) + ├── scripts/{build-main,dev,smoke}.mjs + └── src/ + ├── main/ # 主进程:窗口/单实例/配置持久化(IPC)/冒烟验证 + ├── preload/ # contextBridge 安全桥(不暴露 ipcRenderer 本体) + ├── types/desktop.d.ts # 主进程 <-> 渲染进程契约 + └── renderer/ # 渲染进程:api / stores / cache / router / layouts / views ``` ## 技术栈 -| 层 | 选型 | 版本 | -| ------ | ------------------------------------------------------------------ | ----------------------------------------------------- | -| 包管理 | pnpm workspace | 11.8.0 | -| 语言 | TypeScript | 5.9.3(`typescript-eslint` 要求 < 6.1,因此未用 7.x) | -| 后端 | Node.js + Express | 24.18 / 5.2 | -| ORM | Prisma + driver adapter | 7.10(+ `@prisma/adapter-libsql`) | -| 实时 | Socket.IO | 4.8 | -| 认证 | jsonwebtoken + bcryptjs | 9.0 / 3.0 | -| 校验 | zod | 4.6 | -| Web 端 | Vue 3 + Vite + Element Plus + Pinia + Vue Router + Axios + ECharts | 3.5 / 8.3 / 2.14 / 4.0 / 5.3 / 1.20 / 6.1 | +| 层 | 选型 | 版本 | +| ---------- | ---------------------------------------------------------------------- | ----------------------------------------------------- | +| 包管理 | pnpm workspace | 11.8.0 | +| 语言 | TypeScript | 5.9.3(`typescript-eslint` 要求 < 6.1,因此未用 7.x) | +| 后端 | Node.js + Express | 24.18 / 5.2 | +| ORM | Prisma + driver adapter | 7.10(+ `@prisma/adapter-libsql`) | +| 实时 | Socket.IO | 4.8 | +| 认证 | jsonwebtoken + bcryptjs | 9.0 / 3.0 | +| 校验 | zod | 4.6 | +| Web 端 | Vue 3 + Vite + Element Plus + Pinia + Vue Router + Axios + ECharts | 3.5 / 8.3 / 2.14 / 4.0 / 5.3 / 1.20 / 6.1 | +| EXE 客户端 | Electron + Vue 3 + Element Plus + Pinia + Socket.IO Client + IndexedDB | 44.3 / 3.5 / 2.14 / 4.0 / 4.8 | +| 打包 | electron-builder(nsis / portable) | 26.15 | > 运行环境要求:**Node.js ≥ 20.19**(Prisma 7 硬性要求,推荐 22/24)。 @@ -113,15 +124,32 @@ pnpm dev 也可以分开启动:`pnpm dev:server`(仅后端)、`pnpm dev:web`(仅 Web 端)。 +### 启动 EXE 客户端(学生端) + +```bash +# 开发模式:自动构建主进程 + 启动 Vite 渲染进程(5174) + 打开 Electron 窗口(支持热更新) +pnpm dev:desktop + +# 已构建产物的冒烟验证(不弹窗,跑完自动退出) +pnpm build:desktop +pnpm verify:desktop +``` + +客户端首次启动会带上默认服务器地址 `http://127.0.0.1:4000`,可用学生账号 `student01 / student123` 登录。 +若要验证"断网可查看缓存",登录并浏览过程序后关闭后端(`Ctrl+C` 停掉 `pnpm dev:server`), +客户端会在顶部提示「当前处于离线状态」,页面继续显示本地缓存数据;重新启动后端后会自动同步。 + ### 端到端验收脚本 ```bash # 另开一个终端先启动后端:pnpm dev:server -pnpm verify:e2e +pnpm verify:e2e # 后端 + REST + Socket.IO + RBAC:54 项 +pnpm verify:desktop # Electron 客户端(含联网集成):9 项 ``` -脚本会自动登录教师与学生账号,验证**实时推送时延、作业完成、成绩下发、权限隔离**等 54 项, -并打印逐项结果与耗时(实测:通知 37ms、作业 26ms、成绩 25ms 到达,远优于 5 秒要求)。 +后端脚本验证**实时推送时延、作业完成、成绩下发、权限隔离**等 54 项,实测通知 37ms、作业 26ms、 +成绩 25ms 到达(要求 < 5 秒);客户端脚本验证 preload 桥接、渲染进程、IndexedDB 读写、 +断网回退与联网集成(登录 + 四类数据 + Socket.IO),实测 9/9 通过。 ## 默认账号与种子数据 @@ -144,21 +172,103 @@ pnpm verify:e2e ## 常用命令 -| 命令 | 说明 | -| ------------------------------------------- | ------------------------------------------------------------------- | -| `pnpm dev` | 并行启动 shared(tsc watch) + 后端 + Web 端 | -| `pnpm dev:server` / `pnpm dev:web` | 只启动后端 / 只启动 Web 端 | -| `pnpm build` | 构建 shared + 后端(tsc 产出 `dist`) + Web 端(Vite 产出 `dist`) | -| `pnpm typecheck` | 全仓库类型检查(含 `vue-tsc`) | -| `pnpm lint` / `pnpm lint:fix` | ESLint 检查 / 自动修复 | -| `pnpm format` / `pnpm format:check` | Prettier 格式化 / 检查 | -| `pnpm db:generate` | 生成 Prisma Client(输出到 `packages/server/src/generated/prisma`) | -| `pnpm db:migrate` | 创建并应用迁移 | -| `pnpm db:seed` | 写入种子数据 | -| `pnpm db:reset` | 重置数据库并重新执行 seed | -| `pnpm db:studio` | 打开 Prisma Studio | -| `pnpm db:switch:mysql` / `db:switch:sqlite` | 切换数据库 provider | -| `pnpm verify:e2e` | 端到端验收(需后端已启动) | +| 命令 | 说明 | +| ------------------------------------------- | -------------------------------------------------------------------- | +| `pnpm dev` | 并行启动 shared(tsc watch) + 后端 + Web 端 | +| `pnpm dev:server` / `pnpm dev:web` | 只启动后端 / 只启动 Web 端 | +| `pnpm dev:desktop` | 启动 EXE 客户端开发模式(Vite 5174 + Electron,热更新) | +| `pnpm build` | 构建 shared + 后端 + Web 端 + EXE 客户端 | +| `pnpm build:desktop` | 仅构建 EXE 客户端(esbuild 主进程/preload + Vite 渲染进程) | +| `pnpm verify:desktop` | Electron 冒烟验证(需先 `pnpm build:desktop`;后端在线则含联网集成) | +| `pnpm dist:dir` | 打包免安装目录 `release/win-unpacked`(含可执行文件,最快) | +| `pnpm dist:win` | 打包 nsis 安装包 + portable 单文件 EXE | +| `pnpm typecheck` | 全仓库类型检查(含 `vue-tsc`) | +| `pnpm lint` / `pnpm lint:fix` | ESLint 检查 / 自动修复 | +| `pnpm format` / `pnpm format:check` | Prettier 格式化 / 检查 | +| `pnpm db:generate` | 生成 Prisma Client(输出到 `packages/server/src/generated/prisma`) | +| `pnpm db:migrate` | 创建并应用迁移 | +| `pnpm db:seed` | 写入种子数据 | +| `pnpm db:reset` | 重置数据库并重新执行 seed | +| `pnpm db:studio` | 打开 Prisma Studio | +| `pnpm db:switch:mysql` / `db:switch:sqlite` | 切换数据库 provider | +| `pnpm verify:e2e` | 端到端验收(需后端已启动) | + +## EXE 客户端(学生端) + +### 进程结构与安全设置 + +| 进程 | 文件 | 职责 | +| -------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | +| 主进程 | `src/main/*` → `dist/main/index.js` | 窗口/单实例锁、配置持久化(`safeStorage` 加密 token)、IPC、冒烟验证 | +| 预加载 | `src/preload/index.ts` → `dist/preload/index.js` | 通过 `contextBridge` 暴露最小 API(`getConfig/saveConfig/getAppInfo/openExternal`),不暴露 `ipcRenderer` 本体 | +| 渲染进程 | `src/renderer/**` → `dist/renderer/*` | Vue 3 界面、Axios 请求、Socket.IO 实时、IndexedDB 离线缓存 | + +安全基线:`contextIsolation: true`、`nodeIntegration: false`、`sandbox: true`、`webSecurity: true`, +外部链接一律交给系统浏览器打开,禁止应用内导航到非本地地址。 + +### 功能页面 + +| 页面 | 功能 | +| ---- | ----------------------------------------------------------------------------------------- | +| 登录 | 服务器地址 + 用户名 + 密码,附带「测试连接」;已登录但服务器不可达时可「离线进入」 | +| 课表 | 按周展示 7 列课表,支持上一周/下一周/下拉选择周次,高亮"今天",显示周次范围与地点 | +| 作业 | 列表(全部/未完成/已完成过滤)、详情抽屉、标记完成/取消完成、附件链接、逾期高亮 | +| 通知 | 未读红点(侧边栏徽标)、优先级标签、点击自动标为已读、全部已读、未读过滤、详情抽屉 | +| 成绩 | 表格(分数/得分率进度条/等级)+ ECharts 柱状图(各次考试平均得分率)+ 等级分布 + 最近更新 | +| 设置 | 服务器地址(保存并测试)、账号信息、离线缓存条目统计与清空、客户端版本信息、退出登录 | + +### 离线缓存与自动同步 + +``` + ┌────────────── 在线 ──────────────┐ +请求 → fetchWithCache(store, key, loader, fallback) + └── 失败 → 读取 IndexedDB 缓存 → 返回旧数据(UI 标记"离线缓存") +实时推送(notification:new / grade:updated ...) → 更新内存状态 + 写回 IndexedDB +服务器不可达 → 可达(health 轮询 20s 或任意请求成功)→ 触发 onServerRecovered → 各页自动重新拉取 +``` + +- 缓存按业务域分为 6 个 object store:`profile` / `classes` / `schedules` / `homeworks` / `notifications` / `grades`。 +- 缓存写入前统一做 JSON 纯化(Vue 响应式对象是 Proxy,直接写入 IndexedDB 会抛 `could not be cloned`)。 +- 登录令牌由主进程用 Electron `safeStorage`(Windows 下为 DPAPI)加密后落盘,因此重启客户端可离线进入。 +- 令牌缺失/失效时保持缓存资料可见,恢复网络后自动刷新个人资料。 + +### 打包 Windows EXE + +```bash +pnpm dist:dir # 免安装目录:packages/desktop-client/release/win-unpacked/班级小助手.exe +pnpm dist:win # nsis 安装包 + portable 单文件(需联网下载 NSIS 工具,可用镜像) +``` + +打包脚本 `packages/desktop-client/scripts/dist-win.mjs` 做了三件事,规避常见网络问题: + +1. **复用本地已解压的 Electron**:检测到 `node_modules/electron/dist/electron.exe` 时通过 + `--config.electronDist` 直接使用,不再重复下载 150MB+ 发行包; +2. 默认走国内镜像(可用环境变量覆盖): + `ELECTRON_MIRROR=https://cdn.npmmirror.com/binaries/electron/`、 + `ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/`; +3. 构建缓存固定到仓库内 `.cache/`,不污染全局目录。 + +产物说明:`release/win-unpacked/班级小助手.exe`(约 235MB,Electron 运行时)+ `resources/app.asar` +(约 2.7MB,仅含 `dist/` 与 `package.json`,运行时不需要 node_modules)。 +默认使用 Electron 内置图标(未提供 `build/icon.ico`)。 + +### 客户端冒烟验证(无需人工点击) + +`pnpm verify:desktop` 以 `ELECTRON_SMOKE_TEST=1` 启动应用(不弹窗),主进程依次校验后自动退出: + +| 校验项 | 实测结果 | +| ------------------------------------- | ----------------------------------------------------------------------------------- | +| 渲染进程挂载 + 窗口标题 | ✅ `children=1` / `title=班级小助手` | +| 登录页渲染(三个输入框) | ✅ 服务器地址 / 用户名 / 密码 | +| preload contextBridge 注入 | ✅ 6 个方法 | +| IPC 往返(getAppInfo/saveConfig) | ✅ Electron 44.3.0 | +| 配置文件写入用户目录 | ✅ `%APPDATA%\@classhelper\desktop-client\config.json` | +| IndexedDB 缓存读写 | ✅ 读写/统计/删除 + 6 个 store | +| 断网时回退本地缓存 | ✅ `fromCache=true`(请求不可达端口) | +| 联网集成(`ELECTRON_SMOKE_ONLINE=1`) | ✅ 登录王小明 / 1 班级 / 5 作业 / 4 通知 / 6 成绩 / 第29周29节课 / Socket.IO 已连接 | + +该验证对**开发产物与打包后的 EXE 都适用**(打包后用 `ELECTRON_SMOKE_RESULT=` 写出 JSON 结果, +已实测 `packaged: true`、9/9 通过、退出码 0)。 ## REST API 一览 @@ -283,18 +393,27 @@ pnpm db:migrate && pnpm db:seed ## 验收标准对照 -| 验收项 | 结果 | 证据 | -| --------------------------------------------- | --------- | ----------------------------------------------------------------------------------------- | -| 教师 Web 端发布通知,学生端 5 秒内收到 | ✅ | `verify:e2e`:`notification:new` **37–44ms** | -| 教师发布作业,学生能查看并标记完成 | ✅ | `homework:new` **26–40ms**,`PATCH /homeworks/:id/status` 200 且列表回显 `completed=true` | -| 教师录入成绩,学生能查看个人成绩 | ✅ | `grade:updated` **21–25ms**,`/grades/my` 返回记录;批量录入与统计接口通过 | -| 学生能查看课表,支持按周切换 | ✅ | `/schedules/grid?week=1` 返回 30 节,`week` 过滤 `weekStart ≤ week ≤ weekEnd` | -| 断网后客户端可查看缓存数据 | ⏳ 阶段 4 | 依赖 EXE 客户端(IndexedDB 缓存 + 联网后同步) | -| 权限隔离:学生不能访问其他班级数据 | ✅ | 10 项越权断言全部 403/401(学生跨班/跨班作业/跨班课表、教师跨班发布、未登录访问…) | -| 能成功打包 Windows EXE | ⏳ 阶段 4 | 待 EXE 客户端完成后用 electron-builder 打包 | -| 提供完整 README(启动、构建、打包、默认账号) | 🟡 | 启动/构建/账号已完成;EXE 打包命令随阶段 4 补充 | +| 验收项 | 结果 | 证据 | +| --------------------------------------------- | ---- | ------------------------------------------------------------------------------------------- | +| 教师 Web 端发布通知,学生端 5 秒内收到 | ✅ | `verify:e2e`:`notification:new` **37–44ms** | +| 教师发布作业,学生能查看并标记完成 | ✅ | `homework:new` **26–40ms**,`PATCH /homeworks/:id/status` 200 且列表回显 `completed=true` | +| 教师录入成绩,学生能查看个人成绩 | ✅ | `grade:updated` **21–25ms**,`/grades/my` 返回记录;批量录入与统计接口通过 | +| 学生能查看课表,支持按周切换 | ✅ | `/schedules/grid?week=1` 返回 30 节,`week` 过滤 `weekStart ≤ week ≤ weekEnd` | +| 断网后客户端可查看缓存数据 | ✅ | 客户端冒烟:`断网时回退到本地缓存 → fromCache=true items=1`;离线横幅 + 缓存统计页可用 | +| 权限隔离:学生不能访问其他班级数据 | ✅ | 10 项越权断言全部 403/401(学生跨班/跨班作业/跨班课表、教师跨班发布、未登录访问…) | +| 能成功打包 Windows EXE | ✅ | `班级小助手-0.1.0-x64-setup.exe`(106.9MB) / `-portable.exe`(106.7MB) / `win-unpacked/*.exe` | +| 提供完整 README(启动、构建、打包、默认账号) | ✅ | 本文档含快速开始、命令表、API、WebSocket、RBAC、模块化、MySQL 切换、打包与常见问题 | -当前实测:`pnpm verify:e2e` → **54/54 项通过**;`pnpm typecheck`、`pnpm lint`、`pnpm build` 全部通过。 +打包产物验证(对最终 EXE 实测,非仅开发产物): + +| 产物 | 大小 | 冒烟结果 | +| ---------------------------------------- | ------- | -------------------------------------------------------------- | +| `release/win-unpacked/班级小助手.exe` | 234.7MB | ✅ 9/9,`packaged: true`,退出码 0 | +| `release/…-x64-portable.exe`(单文件) | 106.7MB | ✅ 9/9,含联网集成(登录王小明 / Socket.IO 已连接 / 四类数据) | +| `release/…-x64-setup.exe`(NSIS 安装包) | 106.9MB | ✅ 构建成功(安装后运行同一份 `win-unpacked` 内容) | + +当前实测:`pnpm verify:e2e` → **54/54**;`pnpm verify:desktop` → **9/9**; +`pnpm typecheck`、`pnpm lint`、`pnpm build`、`pnpm dist:win` 全部通过。 ## 常见问题(本机环境已知坑) @@ -325,10 +444,37 @@ pnpm db:migrate && pnpm db:seed 生成器为 `prisma-client`(输出到 `src/generated/prisma`,已 gitignore), 连接串在 `prisma.config.ts` 中配置,且必须通过 driver adapter 接入数据库。 -## 后续计划(阶段 4-6) +6. **Electron 二进制下载慢或失败** + `electron` 的 postinstall 会从 GitHub 拉取约 151MB 发行包,本机实测 10 分钟仍未完成。 + 规避:安装前设置镜像与缓存目录(本项目已把 electron 缓存指向 `.cache/electron`): + `ELECTRON_MIRROR=https://cdn.npmmirror.com/binaries/electron/`。 + 打包阶段 `scripts/dist-win.mjs` 已内置该镜像,并优先复用本地已解压的 Electron。 -1. **阶段 4 · EXE 客户端**:`packages/desktop-client`(Electron 主进程 + Vue 渲染进程), - 登录页(服务器地址/用户名/密码)、侧边栏(课表/作业/通知/成绩/设置)、 - Socket.IO 实时更新、IndexedDB 离线缓存与联网后自动同步、`electron-builder` 打包 Windows EXE。 -2. **阶段 5**:三端联调脚本(含 EXE 端时延与断网恢复用例)、根目录一键打包命令。 -3. **阶段 6**:打包产物分发说明、云端 MySQL 部署清单(Docker Compose)、CI 流水线。 +7. **electron-builder 的构建工具(NSIS / winCodeSign)下载慢** + 通过 `ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/` + 解决(已内置到 `dist-win.mjs`),实测 nsis-3.0.4.1 与 nsis-resources 正常下载。 + +8. **打包未提供图标** + 未放置 `packages/desktop-client/build/icon.ico` 时使用 Electron 默认图标。 + 放入 256×256 及以上尺寸的 `.ico`(或 256×256 PNG)即可自动使用。 + +## 交付清单(阶段 4-6 新增) + +| 路径 | 说明 | +| --------------------------------------------------------------------- | ----------------------------------------------------------- | +| `packages/desktop-client/src/main/*` | Electron 主进程:窗口、单实例、配置持久化、IPC、冒烟验证 | +| `packages/desktop-client/src/preload/index.ts` | contextBridge 安全桥 | +| `packages/desktop-client/src/renderer/api/*` | Axios 封装(动态服务器地址、JWT、离线不弹错) | +| `packages/desktop-client/src/renderer/cache/{db,index}.ts` | IndexedDB 缓存层 + `fetchWithCache` 离线回退 | +| `packages/desktop-client/src/renderer/stores/*` | Pinia:auth / app(可达性与同步)/ realtime / notifications | +| `packages/desktop-client/src/renderer/views/*` | 登录 / 课表 / 作业 / 通知 / 成绩 / 设置 | +| `packages/desktop-client/scripts/{build-main,dev,smoke,dist-win}.mjs` | 构建、开发、冒烟、打包脚本 | +| `packages/desktop-client/electron-builder.yml` | 打包配置(nsis + portable) | + +## 后续可选增强 + +1. 自定义应用图标与安装向导文案(`build/icon.ico`)。 +2. 代码签名证书(消除 SmartScreen 提示)。 +3. 客户端"提交类操作离线队列"(当前离线为只读,联网后自动同步读取的数据)。 +4. 云数据库部署清单(Docker Compose + `prisma migrate deploy`)与 CI 流水线。 +5. 课表按当前时间高亮"正在上的课"与上课提醒(可参考 ClassIsland 的课程提醒设计)。 diff --git a/package.json b/package.json index 6dc57bb..767d5f2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "dev": "pnpm build:shared && concurrently -k -n shared,server,web -c cyan,green,magenta \"pnpm --filter @classhelper/shared dev\" \"pnpm --filter @classhelper/server dev\" \"pnpm --filter @classhelper/web-admin dev\"", "dev:server": "pnpm build:shared && pnpm --filter @classhelper/server dev", "dev:web": "pnpm build:shared && pnpm --filter @classhelper/web-admin dev", - "build": "pnpm build:shared && pnpm --filter @classhelper/server build && pnpm --filter @classhelper/web-admin build", + "dev:desktop": "pnpm build:shared && pnpm --filter @classhelper/desktop-client dev", + "build": "pnpm build:shared && pnpm --filter @classhelper/server build && pnpm --filter @classhelper/web-admin build && pnpm --filter @classhelper/desktop-client build", + "build:desktop": "pnpm build:shared && pnpm --filter @classhelper/desktop-client build", "typecheck": "pnpm build:shared && pnpm -r --no-bail typecheck", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -25,7 +27,10 @@ "db:studio": "pnpm --filter @classhelper/server db:studio", "db:switch:mysql": "node scripts/use-database.mjs mysql", "db:switch:sqlite": "node scripts/use-database.mjs sqlite", - "verify:e2e": "pnpm --filter @classhelper/server verify:e2e" + "verify:e2e": "pnpm --filter @classhelper/server verify:e2e", + "verify:desktop": "pnpm --filter @classhelper/desktop-client smoke", + "dist:win": "pnpm --filter @classhelper/desktop-client dist:win", + "dist:dir": "pnpm --filter @classhelper/desktop-client dist:dir" }, "devDependencies": { "concurrently": "^10.0.5", diff --git a/packages/desktop-client/electron-builder.yml b/packages/desktop-client/electron-builder.yml new file mode 100644 index 0000000..05a0db3 --- /dev/null +++ b/packages/desktop-client/electron-builder.yml @@ -0,0 +1,42 @@ +appId: com.classhelper.desktop +productName: 班级小助手 +copyright: Copyright © 2026 Class Helper + +directories: + output: release + buildResources: build + +# 渲染进程产物已由 Vite 打包、主进程/preload 已由 esbuild 打包, +# 运行时不需要任何 node_modules(electron 模块由 Electron 运行时提供)。 +files: + - dist/**/* + - package.json + - '!node_modules/**/*' + - '!**/*.map' + +asar: true +# 没有原生模块,跳过 rebuild,避免不必要的 node-gyp 依赖 +npmRebuild: false +compression: normal +removePackageScripts: true + +win: + target: + - target: nsis + arch: [x64] + - target: portable + arch: [x64] + # 未提供 build/icon.ico 时使用 Electron 默认图标 + artifactName: ${productName}-${version}-${arch}.${ext} + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: 班级小助手 + artifactName: ${productName}-${version}-${arch}-setup.${ext} + +portable: + artifactName: ${productName}-${version}-${arch}-portable.${ext} diff --git a/packages/desktop-client/index.html b/packages/desktop-client/index.html new file mode 100644 index 0000000..e3815d1 --- /dev/null +++ b/packages/desktop-client/index.html @@ -0,0 +1,13 @@ + + + + + + + 班级小助手 + + +
+ + + diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json new file mode 100644 index 0000000..7e877f6 --- /dev/null +++ b/packages/desktop-client/package.json @@ -0,0 +1,40 @@ +{ + "name": "@classhelper/desktop-client", + "version": "0.1.0", + "private": true, + "description": "班级小助手 EXE 客户端(Electron + Vue 3 + Element Plus + Socket.IO + IndexedDB 离线缓存)", + "author": "Class Helper", + "main": "dist/main/index.js", + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "npm run build:main && npm run build:renderer", + "build:main": "node scripts/build-main.mjs", + "build:renderer": "vite build", + "start": "electron .", + "smoke": "node scripts/smoke.mjs", + "typecheck": "vue-tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json", + "dist:win": "npm run build && node scripts/dist-win.mjs", + "dist:dir": "npm run build && node scripts/dist-win.mjs --dir" + }, + "dependencies": { + "@classhelper/shared": "workspace:*", + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.20.0", + "echarts": "^6.1.0", + "element-plus": "^2.14.5", + "pinia": "^4.0.3", + "socket.io-client": "^4.8.3", + "vue": "^3.5.42", + "vue-router": "^5.3.1" + }, + "devDependencies": { + "@types/node": "^22.20.2", + "@vitejs/plugin-vue": "^6.0.8", + "electron": "^44.3.0", + "electron-builder": "^26.15.3", + "esbuild": "^0.28.2", + "typescript": "~5.9.3", + "vite": "^8.3.0", + "vue-tsc": "^3.3.11" + } +} diff --git a/packages/desktop-client/scripts/build-main.mjs b/packages/desktop-client/scripts/build-main.mjs new file mode 100644 index 0000000..e18e857 --- /dev/null +++ b/packages/desktop-client/scripts/build-main.mjs @@ -0,0 +1,37 @@ +/** + * 主进程 / preload 构建:esbuild 打包为 CommonJS。 + * - external: electron(由 Electron 运行时提供) + * - @classhelper/shared 会被内联打包(ESM 源码 -> CJS 产物) + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { build } from 'esbuild'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +const shared = { + bundle: true, + platform: 'node', + target: 'node20', + format: 'cjs', + sourcemap: false, + external: ['electron'], + logLevel: 'warning', + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'), + }, +}; + +await build({ + ...shared, + entryPoints: [path.join(root, 'src/main/index.ts')], + outfile: path.join(root, 'dist/main/index.js'), +}); + +await build({ + ...shared, + entryPoints: [path.join(root, 'src/preload/index.ts')], + outfile: path.join(root, 'dist/preload/index.js'), +}); + +console.log('[build:main] dist/main/index.js 与 dist/preload/index.js 构建完成'); diff --git a/packages/desktop-client/scripts/dev.mjs b/packages/desktop-client/scripts/dev.mjs new file mode 100644 index 0000000..c6b92f4 --- /dev/null +++ b/packages/desktop-client/scripts/dev.mjs @@ -0,0 +1,67 @@ +/** + * 开发模式:先构建主进程/preload,再启动 Vite 渲染进程开发服务器, + * 等端口就绪后用 Electron 打开开发地址(支持热更新)。 + * + * 说明:子进程统一使用 stdio: 'inherit',在受限沙箱中也能正常启动。 + */ +import { spawn } from 'node:child_process'; +import net from 'node:net'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import electronPath from 'electron'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const HOST = '127.0.0.1'; +const PORT = 5174; +const DEV_URL = `http://${HOST}:${PORT}`; + +const viteBin = path.join(root, 'node_modules', 'vite', 'bin', 'vite.js'); + +const vite = spawn(process.execPath, [viteBin, '--host', HOST, '--port', String(PORT), '--strictPort'], { + cwd: root, + stdio: 'inherit', + env: process.env, +}); + +let electron = null; + +function cleanup(code = 0) { + if (electron && !electron.killed) electron.kill(); + if (!vite.killed) vite.kill(); + process.exit(code); +} + +function waitForPort(attempt = 0) { + const socket = net.connect({ host: HOST, port: PORT }); + socket.once('connect', () => { + socket.destroy(); + startElectron(); + }); + socket.once('error', () => { + socket.destroy(); + if (attempt > 120) { + console.error('[dev] 等待 Vite 开发服务器超时'); + cleanup(1); + return; + } + setTimeout(() => waitForPort(attempt + 1), 250); + }); +} + +function startElectron() { + console.log(`[dev] Vite 已就绪,启动 Electron -> ${DEV_URL}`); + electron = spawn(electronPath, ['.'], { + cwd: root, + stdio: 'inherit', + env: { ...process.env, VITE_DEV_SERVER_URL: DEV_URL }, + }); + electron.on('exit', (code) => cleanup(code ?? 0)); +} + +vite.on('exit', (code) => { + if (code !== 0) console.error(`[dev] Vite 退出,code=${code}`); +}); +process.on('SIGINT', () => cleanup(0)); +process.on('SIGTERM', () => cleanup(0)); + +waitForPort(); diff --git a/packages/desktop-client/scripts/dist-win.mjs b/packages/desktop-client/scripts/dist-win.mjs new file mode 100644 index 0000000..7bb31db --- /dev/null +++ b/packages/desktop-client/scripts/dist-win.mjs @@ -0,0 +1,63 @@ +/** + * Windows 打包入口(等价于 electron-builder --win,但做了三件工程化处理): + * + * 1) 复用本地已解压的 Electron:node_modules/electron/dist 存在时通过 + * --config.electronDist 直接使用,避免再下载 150MB+ 发行包(受网络影响最大的步骤)。 + * 2) 默认使用国内镜像(可用环境变量覆盖): + * ELECTRON_MIRROR Electron 发行包 + * ELECTRON_BUILDER_BINARIES_MIRROR nsis / winCodeSign 等构建工具 + * 3) 缓存固定到仓库内的 .cache 目录,便于清理与迁移,不污染用户全局目录。 + * + * 用法: + * node scripts/dist-win.mjs # nsis 安装包 + portable 单文件 + * node scripts/dist-win.mjs --dir # 仅生成免安装目录 release/win-unpacked + */ +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = path.resolve(root, '..', '..'); +const args = process.argv.slice(2); +const dirOnly = args.includes('--dir'); +const passthrough = args.filter((item) => item !== '--dir'); + +const env = { + ...process.env, + ELECTRON_MIRROR: process.env.ELECTRON_MIRROR ?? 'https://cdn.npmmirror.com/binaries/electron/', + ELECTRON_BUILDER_BINARIES_MIRROR: + process.env.ELECTRON_BUILDER_BINARIES_MIRROR ?? + 'https://npmmirror.com/mirrors/electron-builder-binaries/', + ELECTRON_BUILDER_CACHE: + process.env.ELECTRON_BUILDER_CACHE ?? path.join(repoRoot, '.cache', 'electron-builder'), + ELECTRON_CACHE: process.env.ELECTRON_CACHE ?? path.join(repoRoot, '.cache', 'electron'), +}; + +const localElectronDist = path.join(root, 'node_modules', 'electron', 'dist'); +const overrides = []; +if (fs.existsSync(path.join(localElectronDist, 'electron.exe'))) { + overrides.push(`--config.electronDist=${localElectronDist}`); + console.log('[dist] 复用本地已解压的 Electron:', localElectronDist); +} else { + console.log('[dist] 未找到本地 Electron,将由 electron-builder 下载(镜像:' + env.ELECTRON_MIRROR + ')'); +} + +const cli = path.join(root, 'node_modules', 'electron-builder', 'out', 'cli', 'cli.js'); +if (!fs.existsSync(cli)) { + console.error('[dist] 未找到 electron-builder,请先执行 pnpm install'); + process.exit(2); +} + +const cliArgs = [cli, '--win', ...(dirOnly ? ['--dir'] : []), ...overrides, ...passthrough]; +console.log('[dist] electron-builder', cliArgs.slice(1).join(' ')); + +const child = spawn(process.execPath, cliArgs, { cwd: root, stdio: 'inherit', env }); + +child.on('exit', (code) => { + if (code === 0) { + const output = path.join(root, 'release'); + console.log('\n[dist] 打包完成,产物目录:', output); + } + process.exit(code ?? 1); +}); diff --git a/packages/desktop-client/scripts/smoke.mjs b/packages/desktop-client/scripts/smoke.mjs new file mode 100644 index 0000000..eca3143 --- /dev/null +++ b/packages/desktop-client/scripts/smoke.mjs @@ -0,0 +1,35 @@ +/** + * Electron 冒烟验证:以 ELECTRON_SMOKE_TEST=1 启动应用(不弹窗), + * 主进程会依次校验 preload 桥接、渲染进程挂载、登录页 DOM、 + * IPC 往返、IndexedDB 缓存读写、断网回退缓存,然后自动退出。 + * + * 前置条件:已执行 npm run build(需要 dist/main 与 dist/renderer)。 + */ +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import electronPath from 'electron'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const required = [path.join(root, 'dist/main/index.js'), path.join(root, 'dist/renderer/index.html')]; +const missing = required.filter((file) => !fs.existsSync(file)); + +if (missing.length > 0) { + console.error('[smoke] 缺少构建产物,请先执行构建:'); + console.error(' pnpm --filter @classhelper/desktop-client build'); + for (const file of missing) console.error(` 缺失: ${path.relative(root, file)}`); + process.exit(2); +} + +console.log('[smoke] 启动 Electron 冒烟验证...\n'); +const child = spawn(electronPath, ['.'], { + cwd: root, + stdio: 'inherit', + env: { ...process.env, ELECTRON_SMOKE_TEST: '1' }, +}); + +child.on('exit', (code) => { + console.log(`\n[smoke] Electron 退出,code=${code ?? 1}`); + process.exit(code ?? 1); +}); diff --git a/packages/desktop-client/src/main/config.ts b/packages/desktop-client/src/main/config.ts new file mode 100644 index 0000000..38ec6a8 --- /dev/null +++ b/packages/desktop-client/src/main/config.ts @@ -0,0 +1,110 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { app, safeStorage } from 'electron'; +import { DEFAULT_SERVER_URL } from '@classhelper/shared'; +import type { DesktopStoredConfig } from '../types/desktop.js'; + +interface PersistedConfig { + serverUrl: string; + username: string; + /** 加密后的 token(base64)或明文(当系统不支持加密时) */ + token: string | null; + tokenEncrypted: boolean; +} + +const DEFAULT_CONFIG: PersistedConfig = { + serverUrl: DEFAULT_SERVER_URL, + username: '', + token: null, + tokenEncrypted: false, +}; + +function configFilePath(): string { + return path.join(app.getPath('userData'), 'config.json'); +} + +function encryptToken(token: string | null): { token: string | null; tokenEncrypted: boolean } { + if (!token) return { token: null, tokenEncrypted: false }; + try { + if (safeStorage.isEncryptionAvailable()) { + return { token: safeStorage.encryptString(token).toString('base64'), tokenEncrypted: true }; + } + } catch { + // 某些环境下 DPAPI 不可用,降级为明文存储(仅本地开发场景) + } + return { token, tokenEncrypted: false }; +} + +function decryptToken(raw: PersistedConfig): string | null { + if (!raw.token) return null; + if (!raw.tokenEncrypted) return raw.token; + try { + return safeStorage.decryptString(Buffer.from(raw.token, 'base64')); + } catch { + return null; + } +} + +function readPersisted(): PersistedConfig { + try { + const content = fs.readFileSync(configFilePath(), 'utf8'); + const parsed = JSON.parse(content) as Partial; + return { + serverUrl: + typeof parsed.serverUrl === 'string' && parsed.serverUrl + ? parsed.serverUrl + : DEFAULT_CONFIG.serverUrl, + username: typeof parsed.username === 'string' ? parsed.username : '', + token: typeof parsed.token === 'string' ? parsed.token : null, + tokenEncrypted: parsed.tokenEncrypted === true, + }; + } catch { + return { ...DEFAULT_CONFIG }; + } +} + +function writePersisted(config: PersistedConfig): void { + const target = configFilePath(); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, JSON.stringify(config, null, 2), 'utf8'); +} + +/** 读取配置(token 已解密),供渲染进程使用 */ +export function getConfig(): DesktopStoredConfig { + const persisted = readPersisted(); + return { + serverUrl: persisted.serverUrl, + username: persisted.username, + token: decryptToken(persisted), + }; +} + +/** 局部更新配置:只覆盖传入的字段 */ +export function saveConfig(patch: Partial): DesktopStoredConfig { + const persisted = readPersisted(); + + if (typeof patch.serverUrl === 'string' && patch.serverUrl.trim()) { + persisted.serverUrl = patch.serverUrl.trim().replace(/\/+$/, ''); + } + if (typeof patch.username === 'string') { + persisted.username = patch.username; + } + if (patch.token !== undefined) { + const encrypted = encryptToken(patch.token); + persisted.token = encrypted.token; + persisted.tokenEncrypted = encrypted.tokenEncrypted; + } + + writePersisted(persisted); + return getConfig(); +} + +/** 清空为默认配置(退出登录 / 重置) */ +export function clearConfig(): DesktopStoredConfig { + writePersisted({ ...DEFAULT_CONFIG }); + return getConfig(); +} + +export function getConfigPath(): string { + return configFilePath(); +} diff --git a/packages/desktop-client/src/main/index.ts b/packages/desktop-client/src/main/index.ts new file mode 100644 index 0000000..90a66c2 --- /dev/null +++ b/packages/desktop-client/src/main/index.ts @@ -0,0 +1,102 @@ +import path from 'node:path'; +import { BrowserWindow, app, shell } from 'electron'; +import { registerIpcHandlers } from './ipc.js'; +import { runSmokeTest } from './smoke.js'; + +// 主进程由 esbuild 打包为 CommonJS,因此这里可以直接使用 __dirname +// (dist/main/index.js -> dist/preload/index.js 与 dist/renderer/index.html) +const currentDir = __dirname; + +const devServerUrl = process.env.VITE_DEV_SERVER_URL; +const isSmokeTest = process.env.ELECTRON_SMOKE_TEST === '1'; + +let mainWindow: BrowserWindow | null = null; + +function createWindow(): BrowserWindow { + const win = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 980, + minHeight: 640, + title: '班级小助手', + // 冒烟验证时不弹窗,避免打扰使用者 + show: !isSmokeTest, + autoHideMenuBar: true, + backgroundColor: '#f5f7fa', + webPreferences: { + preload: path.join(currentDir, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + webSecurity: true, + }, + }); + + // 外部链接交给系统浏览器,禁止在应用内新开窗口 + win.webContents.setWindowOpenHandler(({ url }) => { + if (/^https?:\/\//i.test(url)) void shell.openExternal(url); + return { action: 'deny' }; + }); + win.webContents.on('will-navigate', (event, url) => { + const allowed = devServerUrl ? url.startsWith(devServerUrl) : url.startsWith('file://'); + if (!allowed) { + event.preventDefault(); + if (/^https?:\/\//i.test(url)) void shell.openExternal(url); + } + }); + + win.on('closed', () => { + mainWindow = null; + }); + + return win; +} + +async function loadRenderer(win: BrowserWindow): Promise { + if (devServerUrl) { + await win.loadURL(devServerUrl); + win.webContents.openDevTools({ mode: 'detach' }); + return; + } + // 生产:加载打包后的本地 HTML(vite base 为 './',使用 file:// 相对路径) + await win.loadFile(path.join(currentDir, '../renderer/index.html')); +} + +// 单实例:重复启动时聚焦已有窗口 +const gotLock = app.requestSingleInstanceLock(); +if (!gotLock) { + app.quit(); +} else { + app.on('second-instance', () => { + if (!mainWindow) return; + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + }); + + app.whenReady().then(async () => { + registerIpcHandlers(); + + mainWindow = createWindow(); + await loadRenderer(mainWindow); + + if (isSmokeTest) { + try { + await runSmokeTest(mainWindow); + } catch (error) { + console.error('[SMOKE] 冒烟验证异常', error); + app.exit(1); + } + } + }); + + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); + }); + + app.on('activate', async () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + await loadRenderer(mainWindow); + } + }); +} diff --git a/packages/desktop-client/src/main/ipc.ts b/packages/desktop-client/src/main/ipc.ts new file mode 100644 index 0000000..3c412e2 --- /dev/null +++ b/packages/desktop-client/src/main/ipc.ts @@ -0,0 +1,45 @@ +import { app, ipcMain, shell } from 'electron'; +import { clearConfig, getConfig, getConfigPath, saveConfig } from './config.js'; +import type { DesktopAppInfo, DesktopStoredConfig } from '../types/desktop.js'; + +/** IPC 通道名集中管理,preload 与主进程共用同一份字符串 */ +export const IPC_CHANNELS = { + getConfig: 'classhelper:config:get', + saveConfig: 'classhelper:config:save', + clearConfig: 'classhelper:config:clear', + appInfo: 'classhelper:app:info', + openExternal: 'classhelper:shell:open-external', +} as const; + +export function registerIpcHandlers(): void { + ipcMain.handle(IPC_CHANNELS.getConfig, (): DesktopStoredConfig => getConfig()); + + ipcMain.handle( + IPC_CHANNELS.saveConfig, + (_event, patch: Partial): DesktopStoredConfig => { + return saveConfig(patch ?? {}); + }, + ); + + ipcMain.handle(IPC_CHANNELS.clearConfig, (): DesktopStoredConfig => clearConfig()); + + ipcMain.handle(IPC_CHANNELS.appInfo, (): DesktopAppInfo => ({ + appVersion: app.getVersion(), + electron: process.versions.electron ?? '', + chrome: process.versions.chrome ?? '', + node: process.versions.node ?? '', + platform: `${process.platform} ${process.arch}`, + userDataPath: app.getPath('userData'), + smokeTest: process.env.ELECTRON_SMOKE_TEST === '1', + })); + + ipcMain.handle(IPC_CHANNELS.openExternal, async (_event, url: string): Promise => { + if (!/^https?:\/\//i.test(url)) return false; + await shell.openExternal(url); + return true; + }); +} + +export function getDiagnostics(): { configPath: string; config: DesktopStoredConfig } { + return { configPath: getConfigPath(), config: getConfig() }; +} diff --git a/packages/desktop-client/src/main/smoke.ts b/packages/desktop-client/src/main/smoke.ts new file mode 100644 index 0000000..3bff640 --- /dev/null +++ b/packages/desktop-client/src/main/smoke.ts @@ -0,0 +1,161 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { app } from 'electron'; +import type { BrowserWindow } from 'electron'; +import { getDiagnostics } from './ipc.js'; + +interface SmokeResult { + name: string; + ok: boolean; + detail: string; +} + +/** + * Electron 冒烟验证(ELECTRON_SMOKE_TEST=1 时触发,跑完自动退出)。 + * 覆盖:preload 桥接、渲染进程挂载、登录页 DOM、IndexedDB 缓存读写、离线回退, + * 以及可选的联网集成(ELECTRON_SMOKE_ONLINE=1)。 + * + * 结果同时输出到 stdout,并在设置 ELECTRON_SMOKE_RESULT 时写入 JSON 文件 —— + * 打包后的 Windows GUI 进程没有 stdout,用结果文件才能在 CI/脚本里验证安装包。 + */ +export async function runSmokeTest(win: BrowserWindow): Promise { + const results: SmokeResult[] = []; + const record = (name: string, ok: boolean, detail = ''): void => { + results.push({ name, ok, detail }); + console.log(`${ok ? '[PASS]' : '[FAIL]'} ${name}${detail ? ` - ${detail}` : ''}`); + }; + + // 等待 Vue 挂载并完成启动流程(离线时最多等 14 秒) + let dom: { + title: string; + appChildren: number; + hasLoginPage: boolean; + hasInput: boolean; + text: string; + } | null = null; + for (let attempt = 0; attempt < 56; attempt += 1) { + dom = await win.webContents.executeJavaScript( + `(() => { + const app = document.querySelector('#app'); + return { + title: document.title, + appChildren: app ? app.children.length : 0, + hasLoginPage: Boolean(document.querySelector('.login-page')), + hasInput: Boolean(document.querySelector('input')), + text: (document.body.innerText || '').replace(/\\s+/g, ' ').slice(0, 80), + }; + })()`, + ); + // 等到登录页真正渲染出来(而不是停在启动过渡页) + if (dom?.hasLoginPage && dom?.hasInput) break; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + record('渲染进程挂载(#app 有子节点)', (dom?.appChildren ?? 0) > 0, `children=${dom?.appChildren ?? 0}`); + record( + '窗口标题正确', + dom?.title === '班级小助手' || (dom?.title ?? '').includes('班级小助手'), + `title=${dom?.title}`, + ); + record( + '登录页渲染(服务器地址/用户名/密码输入框)', + Boolean(dom?.hasLoginPage && dom?.hasInput), + `text=${dom?.text ?? ''}`, + ); + + // preload 桥接 + const bridge = await win.webContents.executeJavaScript( + `(() => ({ + hasBridge: typeof window.desktop === 'object' && window.desktop !== null, + methods: Object.keys(window.desktop ?? {}).sort(), + }))()`, + ); + record( + 'preload contextBridge 注入', + Boolean(bridge?.hasBridge) && Array.isArray(bridge?.methods) && bridge.methods.includes('getConfig'), + `methods=${(bridge?.methods ?? []).join(',')}`, + ); + + // IPC 往返(渲染进程 <-> 主进程) + const ipcRoundTrip = await win.webContents.executeJavaScript( + `(async () => { + const info = await window.desktop.getAppInfo(); + const saved = await window.desktop.saveConfig({ serverUrl: 'http://127.0.0.1:4000' }); + return { electron: info.electron, hasSmokeFlag: info.smokeTest === true, serverUrl: saved.serverUrl, userData: info.userDataPath }; + })()`, + ); + record( + 'IPC 调用主进程(getAppInfo / saveConfig)', + Boolean(ipcRoundTrip?.electron) && ipcRoundTrip?.serverUrl === 'http://127.0.0.1:4000', + `electron=${ipcRoundTrip?.electron} serverUrl=${ipcRoundTrip?.serverUrl}`, + ); + + const diagnostics = getDiagnostics(); + record('配置文件已写入用户目录', diagnostics.config.serverUrl.length > 0, diagnostics.configPath); + + // IndexedDB 缓存自检 + const cacheSelfTest = await win.webContents.executeJavaScript( + `(async () => { + if (!window.__classhelperSmoke__) return { ok: false, detail: '渲染进程未注册冒烟钩子' }; + return await window.__classhelperSmoke__.cacheSelfTest(); + })()`, + ); + record('IndexedDB 缓存读写', Boolean(cacheSelfTest?.ok), String(cacheSelfTest?.detail ?? '')); + + // 离线回退场景 + const offline = await win.webContents.executeJavaScript( + `(async () => { + if (!window.__classhelperSmoke__) return { ok: false, detail: '渲染进程未注册冒烟钩子' }; + return await window.__classhelperSmoke__.offlineScenario(); + })()`, + ); + record('断网时回退到本地缓存', Boolean(offline?.ok), String(offline?.detail ?? '')); + + // 可选:对真实后端做联网集成自检(ELECTRON_SMOKE_ONLINE=1 时启用) + if (process.env.ELECTRON_SMOKE_ONLINE === '1') { + const online = await win.webContents.executeJavaScript( + `(async () => { + if (!window.__classhelperSmoke__) return { ok: false, detail: '渲染进程未注册冒烟钩子' }; + return await window.__classhelperSmoke__.onlineScenario(); + })()`, + ); + record( + '联网集成(登录 + 四类数据 + Socket.IO + 缓存写入)', + Boolean(online?.ok), + String(online?.detail ?? ''), + ); + } + + const failed = results.filter((item) => !item.ok); + const passed = results.length - failed.length; + console.log(`\n[SMOKE] ${passed}/${results.length} 项通过`); + + const resultFile = process.env.ELECTRON_SMOKE_RESULT; + if (resultFile) { + try { + fs.mkdirSync(path.dirname(resultFile), { recursive: true }); + fs.writeFileSync( + resultFile, + JSON.stringify( + { + passed, + total: results.length, + ok: failed.length === 0, + results, + finishedAt: new Date().toISOString(), + appVersion: app.getVersion(), + packaged: app.isPackaged, + }, + null, + 2, + ), + 'utf8', + ); + console.log(`[SMOKE] 结果已写入 ${resultFile}`); + } catch (error) { + console.error('[SMOKE] 写入结果文件失败', error); + } + } + + app.exit(failed.length === 0 ? 0 : 1); +} diff --git a/packages/desktop-client/src/preload/index.ts b/packages/desktop-client/src/preload/index.ts new file mode 100644 index 0000000..f4f3c27 --- /dev/null +++ b/packages/desktop-client/src/preload/index.ts @@ -0,0 +1,19 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import { IPC_CHANNELS } from '../main/ipc.js'; +import type { DesktopAppInfo, DesktopBridge, DesktopStoredConfig } from '../types/desktop.js'; + +/** + * 预加载脚本:在开启了 contextIsolation + sandbox 的前提下, + * 只向渲染进程暴露一组最小的、显式声明的能力(不暴露 ipcRenderer 本体)。 + */ +const bridge: DesktopBridge = { + smokeTest: process.env.ELECTRON_SMOKE_TEST === '1', + getConfig: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.getConfig), + saveConfig: (patch: Partial): Promise => + ipcRenderer.invoke(IPC_CHANNELS.saveConfig, patch), + clearConfig: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.clearConfig), + getAppInfo: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.appInfo), + openExternal: (url: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.openExternal, url), +}; + +contextBridge.exposeInMainWorld('desktop', bridge); diff --git a/packages/desktop-client/src/renderer/App.vue b/packages/desktop-client/src/renderer/App.vue new file mode 100644 index 0000000..2efe053 --- /dev/null +++ b/packages/desktop-client/src/renderer/App.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/packages/desktop-client/src/renderer/api/http.ts b/packages/desktop-client/src/renderer/api/http.ts new file mode 100644 index 0000000..d222501 --- /dev/null +++ b/packages/desktop-client/src/renderer/api/http.ts @@ -0,0 +1,95 @@ +import axios, { type AxiosError, type AxiosRequestConfig } from 'axios'; +import { ElMessage } from 'element-plus'; +import type { ApiErrorResponse, ApiResponse } from '@classhelper/shared'; +import { apiBaseOf } from '../config.js'; + +export const http = axios.create({ timeout: 15000 }); + +/** 服务器地址变化时更新 baseURL(登录页/设置页可改) */ +export function setApiBaseUrl(serverUrl: string): void { + http.defaults.baseURL = apiBaseOf(serverUrl); +} + +let tokenProvider: () => string | null = () => null; +export function setTokenProvider(provider: () => string | null): void { + tokenProvider = provider; +} + +let unauthorizedHandler: () => void = () => undefined; +export function setUnauthorizedHandler(handler: () => void): void { + unauthorizedHandler = handler; +} + +let reachabilityReporter: (reachable: boolean) => void = () => undefined; +export function setReachabilityReporter(reporter: (reachable: boolean) => void): void { + reachabilityReporter = reporter; +} + +/** 判断是否为「网络不可达」(用于离线判定,不弹提示) */ +export function isNetworkError(error: unknown): boolean { + const axiosError = error as AxiosError | undefined; + return Boolean(axiosError && !axiosError.response); +} + +http.interceptors.request.use((config) => { + const token = tokenProvider(); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; +}); + +http.interceptors.response.use( + (response) => { + reachabilityReporter(true); + const body = response.data as ApiResponse | undefined; + if (body && typeof body === 'object' && 'success' in body) { + if (!body.success) { + ElMessage.error(body.message || '请求失败'); + return Promise.reject(new Error(body.message || '请求失败')); + } + return body.data as never; + } + return response.data as never; + }, + (error: AxiosError) => { + // 网络不可达:交给调用方走离线缓存,不打扰用户 + if (!error.response) { + reachabilityReporter(false); + return Promise.reject(error); + } + + reachabilityReporter(true); + const status = error.response.status; + const message = error.response.data?.message ?? `请求失败(${status})`; + + if (status === 401) { + ElMessage.warning('登录状态已失效,请重新登录'); + unauthorizedHandler(); + } else { + ElMessage.error(message); + } + return Promise.reject(error); + }, +); + +async function request(config: AxiosRequestConfig): Promise { + return (await http.request(config)) as unknown as T; +} + +export const api = { + get: (url: string, params?: Record): Promise => + request({ method: 'GET', url, params }), + post: (url: string, data?: unknown): Promise => request({ method: 'POST', url, data }), + patch: (url: string, data?: unknown): Promise => request({ method: 'PATCH', url, data }), + delete: (url: string): Promise => request({ method: 'DELETE', url }), +}; + +/** 健康检查(用于可达性轮询,不弹任何提示) */ +export async function pingHealth(): Promise<{ ok: boolean; status?: string }> { + try { + const base = http.defaults.baseURL ?? ''; + const response = await axios.get(`${base}/health`, { timeout: 5000 }); + return { ok: true, status: response.data?.data?.status as string | undefined }; + } catch { + return { ok: false }; + } +} diff --git a/packages/desktop-client/src/renderer/api/index.ts b/packages/desktop-client/src/renderer/api/index.ts new file mode 100644 index 0000000..c1a1b46 --- /dev/null +++ b/packages/desktop-client/src/renderer/api/index.ts @@ -0,0 +1,61 @@ +import { + API_PATHS, + type ClassDetailDto, + type ClassDto, + type DashboardSummary, + type GradeDto, + type HomeworkDto, + type HomeworkStatusDto, + type LoginRequest, + type LoginResponse, + type NotificationDto, + type ScheduleDto, + type ScheduleWeekView, + type StudentDto, +} from '@classhelper/shared'; +import { api } from './http.js'; + +/** 客户端只需用到学生视角的这批接口(与后端同一套 REST 契约) */ +export const authApi = { + login: (payload: LoginRequest): Promise => api.post(API_PATHS.auth.login, payload), + logout: (): Promise<{ loggedOut: boolean }> => api.post(API_PATHS.auth.logout, {}), + me: (): Promise => api.get(API_PATHS.auth.me), +}; + +export const classApi = { + list: (): Promise => api.get(API_PATHS.classes), + detail: (id: string): Promise => api.get(`${API_PATHS.classes}/${id}`), +}; + +export const scheduleApi = { + list: (params: { classId?: string; week?: number }): Promise => + api.get(API_PATHS.schedules, params), + grid: (params: { classId?: string; week?: number }): Promise => + api.get(`${API_PATHS.schedules}/grid`, params), +}; + +export const homeworkApi = { + list: (params?: { classId?: string; pendingOnly?: boolean; courseId?: string }): Promise => + api.get(API_PATHS.homeworks, params), + detail: (id: string): Promise => api.get(`${API_PATHS.homeworks}/${id}`), + updateStatus: (id: string, completed: boolean): Promise => + api.patch(`${API_PATHS.homeworks}/${id}/status`, { completed }), +}; + +export const notificationApi = { + list: (params?: { classId?: string; unreadOnly?: boolean }): Promise => + api.get(API_PATHS.notifications, params), + unreadCount: (): Promise<{ count: number }> => api.get(`${API_PATHS.notifications}/unread-count`), + markRead: (id: string): Promise<{ notificationId: string; readAt: string }> => + api.post(`${API_PATHS.notifications}/${id}/read`, {}), + markAllRead: (): Promise<{ marked: number }> => api.post(`${API_PATHS.notifications}/read-all`, {}), +}; + +export const gradeApi = { + my: (): Promise => api.get(`${API_PATHS.grades}/my`), +}; + +export const dashboardApi = { + summary: (): Promise => api.get(API_PATHS.dashboard), + term: (): Promise<{ currentWeek: number; maxWeek: number }> => api.get('/dashboard/term'), +}; diff --git a/packages/desktop-client/src/renderer/cache/db.ts b/packages/desktop-client/src/renderer/cache/db.ts new file mode 100644 index 0000000..6c26abe --- /dev/null +++ b/packages/desktop-client/src/renderer/cache/db.ts @@ -0,0 +1,133 @@ +/** + * IndexedDB 缓存层(无第三方依赖)。 + * + * 设计: + * - 一个数据库 classhelper-cache,按业务域拆分为多个 object store + * - 每条记录 = { key, value, updatedAt },key 形如 `${classId}` / `${classId}:${week}` + * - 所有操作都包成 Promise;任何一步失败都不会抛出到 UI(离线时降级为空) + */ + +export const CACHE_STORES = [ + 'profile', + 'classes', + 'schedules', + 'homeworks', + 'notifications', + 'grades', +] as const; +export type CacheStore = (typeof CACHE_STORES)[number]; + +export interface CacheRecord { + key: string; + value: T; + updatedAt: number; +} + +export interface CacheStoreStat { + store: CacheStore; + count: number; + updatedAt: number | null; +} + +const DB_NAME = 'classhelper-cache'; +const DB_VERSION = 1; + +let databasePromise: Promise | null = null; + +function openDatabase(): Promise { + if (databasePromise) return databasePromise; + + databasePromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onupgradeneeded = () => { + const db = request.result; + for (const store of CACHE_STORES) { + if (!db.objectStoreNames.contains(store)) { + db.createObjectStore(store, { keyPath: 'key' }); + } + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error ?? new Error('IndexedDB 打开失败')); + request.onblocked = () => reject(new Error('IndexedDB 被其它窗口占用')); + }); + + return databasePromise; +} + +function runRequest( + store: CacheStore, + mode: IDBTransactionMode, + action: (objectStore: IDBObjectStore) => IDBRequest, +): Promise { + return openDatabase().then( + (db) => + new Promise((resolve, reject) => { + const transaction = db.transaction(store, mode); + const request = action(transaction.objectStore(store)); + request.onsuccess = () => resolve(request.result as T); + request.onerror = () => reject(request.error ?? new Error('IndexedDB 操作失败')); + }), + ); +} + +/** + * 转成可被 IndexedDB 结构化克隆的纯数据。 + * + * 关键:Vue 的 reactive/ref 包装对象是 Proxy,直接 put 会抛 + * "could not be cloned"。缓存内容都是后端 JSON 响应,因此用 JSON 往返即可, + * 既去掉代理又保证与 DTO(日期为 ISO 字符串)一致。 + */ +function toPlain(value: T): T { + if (value === undefined) return null as unknown as T; + try { + return JSON.parse(JSON.stringify(value)) as T; + } catch { + return value; + } +} + +export async function cacheSet(store: CacheStore, key: string, value: T): Promise { + const record: CacheRecord = { key, value: toPlain(value), updatedAt: Date.now() }; + await runRequest(store, 'readwrite', (objectStore) => objectStore.put(record)); +} + +export function cacheGet(store: CacheStore, key: string): Promise | null> { + return runRequest | undefined>(store, 'readonly', (objectStore) => + objectStore.get(key), + ).then((record) => record ?? null); +} + +export function cacheList(store: CacheStore): Promise>> { + return runRequest>>(store, 'readonly', (objectStore) => objectStore.getAll()); +} + +export async function cacheRemove(store: CacheStore, key: string): Promise { + await runRequest(store, 'readwrite', (objectStore) => objectStore.delete(key)); +} + +export async function cacheClearStore(store: CacheStore): Promise { + await runRequest(store, 'readwrite', (objectStore) => objectStore.clear()); +} + +export async function cacheClearAll(): Promise { + for (const store of CACHE_STORES) { + await cacheClearStore(store); + } +} + +/** 各业务域的条目数与最近更新时间,用于「设置」页展示离线缓存状况 */ +export async function cacheStats(): Promise { + const stats: CacheStoreStat[] = []; + for (const store of CACHE_STORES) { + const records = await cacheList(store); + const latest = records.reduce( + (max, record) => (max === null || record.updatedAt > max ? record.updatedAt : max), + null, + ); + stats.push({ store, count: records.length, updatedAt: latest }); + } + return stats; +} diff --git a/packages/desktop-client/src/renderer/cache/index.ts b/packages/desktop-client/src/renderer/cache/index.ts new file mode 100644 index 0000000..a35b4d4 --- /dev/null +++ b/packages/desktop-client/src/renderer/cache/index.ts @@ -0,0 +1,54 @@ +import { cacheGet, cacheSet, type CacheStore } from './db.js'; + +export interface CachedFetchResult { + data: T; + /** true 表示本次数据来自本地缓存(网络失败或未联网) */ + fromCache: boolean; + /** 缓存写入时间(来自缓存时有效) */ + updatedAt: number | null; + /** 网络失败原因(成功时为 null) */ + error: string | null; +} + +/** + * 带离线回退的取数: + * 1) 先请求网络,成功则写入缓存并返回 + * 2) 网络失败则读取缓存;缓存缺失时返回调用方提供的兜底值 + * + * 「联网后自动同步」由调用方在重连事件里再次调用本函数完成。 + */ +export async function fetchWithCache( + store: CacheStore, + key: string, + loader: () => Promise, + fallback: T, +): Promise> { + try { + const data = await loader(); + await cacheSet(store, key, data); + return { data, fromCache: false, updatedAt: Date.now(), error: null }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const cached = await cacheGet(store, key).catch(() => null); + if (cached) { + return { data: cached.value, fromCache: true, updatedAt: cached.updatedAt, error: message }; + } + return { data: fallback, fromCache: false, updatedAt: null, error: message }; + } +} + +/** 只读缓存(不再发请求),用于启动时先渲染旧数据 */ +export async function readCache( + store: CacheStore, + key: string, + fallback: T, +): Promise> { + const cached = await cacheGet(store, key).catch(() => null); + if (cached) return { data: cached.value, fromCache: true, updatedAt: cached.updatedAt, error: null }; + return { data: fallback, fromCache: false, updatedAt: null, error: null }; +} + +/** 把实时推送的数据直接写入缓存,保证离线时看到的是最新内容 */ +export async function writeCache(store: CacheStore, key: string, value: T): Promise { + await cacheSet(store, key, value); +} diff --git a/packages/desktop-client/src/renderer/cache/self-test.ts b/packages/desktop-client/src/renderer/cache/self-test.ts new file mode 100644 index 0000000..fdc0c5d --- /dev/null +++ b/packages/desktop-client/src/renderer/cache/self-test.ts @@ -0,0 +1,152 @@ +import { cacheGet, cacheRemove, cacheSet, cacheStats } from './db.js'; +import { fetchWithCache } from './index.js'; + +export interface SmokeCheckResult { + ok: boolean; + detail: string; +} + +/** + * 冒烟自检 1:IndexedDB 写入 / 读回 / 统计 / 删除。 + * 由主进程通过 webContents.executeJavaScript 调用。 + */ +export async function cacheSelfTest(): Promise { + const key = '__smoke_selftest__'; + const payload = { text: 'ok', at: Date.now() }; + + await cacheSet('profile', key, payload); + const readBack = await cacheGet('profile', key); + const stats = await cacheStats(); + await cacheRemove('profile', key); + const removed = await cacheGet('profile', key); + + const ok = readBack?.value.text === 'ok' && removed === null && stats.length > 0; + return { + ok, + detail: `readBack=${readBack?.value.text ?? '-'} removed=${removed === null} stores=${stats.length}`, + }; +} + +/** + * 冒烟自检 2:断网回退。 + * 先写入缓存,再向一个不存在的端口发请求(必然失败), + * 验证 fetchWithCache 能回退到本地缓存。 + */ +export async function offlineScenario(): Promise { + const key = '__smoke_offline__'; + const payload = [{ id: 'offline-1', title: '离线缓存中的作业' }]; + + await cacheSet('homeworks', key, payload); + + const result = await fetchWithCache>( + 'homeworks', + key, + async () => { + const response = await fetch('http://127.0.0.1:59999/api/homeworks'); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return (await response.json()) as Array<{ id: string; title: string }>; + }, + [], + ); + + await cacheRemove('homeworks', key); + + const ok = result.fromCache === true && result.data.length === 1 && Boolean(result.error); + return { + ok, + detail: `fromCache=${result.fromCache} items=${result.data.length} error=${result.error ?? '-'}`, + }; +} + +/** + * 冒烟自检 3(需后端在线):真实登录 + 拉取四类数据 + Socket.IO 连接。 + * 覆盖渲染进程里实际使用的 API 封装、JWT 注入、实时通道与缓存写入。 + */ +export async function onlineScenario(): Promise { + const [{ useAuthStore }, { useAppStore }, { useRealtimeStore }, api, cache] = await Promise.all([ + import('../stores/auth.js'), + import('../stores/app.js'), + import('../stores/realtime.js'), + import('../api/index.js'), + import('./index.js'), + ]); + + const auth = useAuthStore(); + const appStore = useAppStore(); + const realtime = useRealtimeStore(); + + const serverUrl = 'http://127.0.0.1:4000'; + const detail: string[] = []; + + try { + await auth.login(serverUrl, 'student01', 'student123'); + detail.push(`login=${auth.user?.name ?? '-'}`); + + await appStore.init(serverUrl); + detail.push(`reachable=${appStore.serverReachable}`); + + const [classes, homeworks, notifications, grades] = await Promise.all([ + api.classApi.list(), + api.homeworkApi.list(), + api.notificationApi.list(), + api.gradeApi.my(), + ]); + const term = await api.dashboardApi.term(); + const schedules = await api.scheduleApi.list({ week: term.currentWeek }); + + detail.push(`classes=${classes.length}`); + detail.push(`homeworks=${homeworks.length}`); + detail.push(`notifications=${notifications.length}`); + detail.push(`grades=${grades.length}`); + detail.push(`week${term.currentWeek}Schedules=${schedules.length}`); + + // 写入缓存(离线可用的前提) + await cache.writeCache('homeworks', 'self', homeworks); + await cache.writeCache('grades', 'my', grades); + await cache.writeCache('notifications', 'self', notifications); + + // Socket.IO:连接并等待服务端回执 + if (auth.token) { + realtime.connect(serverUrl, auth.token); + const connected = await new Promise((resolve) => { + const timer = setTimeout(() => resolve(false), 8000); + const handler = (): void => { + clearTimeout(timer); + resolve(true); + }; + realtime.on('__connected__', handler); + if (realtime.connected) handler(); + }); + detail.push(`socket=${connected}`); + if (!connected) { + realtime.disconnect(); + await auth.logout(); + return { ok: false, detail: `${detail.join(' ')} (实时通道未连接)` }; + } + } + + const ok = + classes.length === 1 && + schedules.length > 0 && + homeworks.length > 0 && + notifications.length > 0 && + grades.length > 0 && + realtime.connected; + + realtime.disconnect(); + await auth.logout(); // 清理登录态,保证下次冒烟从登录页开始 + return { ok, detail: detail.join(' ') }; + } catch (error) { + realtime.disconnect(); + await auth.logout().catch(() => undefined); + return { + ok: false, + detail: `${detail.join(' ')} error=${error instanceof Error ? error.message : String(error)}`, + }; + } +} + +/** 注册到 window,供主进程冒烟脚本调用 */ +export function registerSmokeHooks(): void { + window.__classhelperSmoke__ = { cacheSelfTest, offlineScenario, onlineScenario }; +} diff --git a/packages/desktop-client/src/renderer/config.ts b/packages/desktop-client/src/renderer/config.ts new file mode 100644 index 0000000..517453c --- /dev/null +++ b/packages/desktop-client/src/renderer/config.ts @@ -0,0 +1,26 @@ +import { DEFAULT_SERVER_URL } from '@classhelper/shared'; + +/** 默认后端地址(首次启动的预填值) */ +export const DEFAULT_SERVER = DEFAULT_SERVER_URL; + +/** 由服务器地址推导 API 基地址,例如 http://127.0.0.1:4000 -> http://127.0.0.1:4000/api */ +export function apiBaseOf(serverUrl: string): string { + return `${serverUrl.trim().replace(/\/+$/, '')}/api`; +} + +/** 由服务器地址推导 Socket.IO 地址 */ +export function socketUrlOf(serverUrl: string): string { + return serverUrl.trim().replace(/\/+$/, ''); +} + +/** 校验用户输入的服务器地址 */ +export function normalizeServerUrl(input: string): string | null { + const value = input.trim().replace(/\/+$/, ''); + if (!/^https?:\/\/[^\s]+$/i.test(value)) return null; + try { + const url = new URL(value); + return `${url.protocol}//${url.host}`; + } catch { + return null; + } +} diff --git a/packages/desktop-client/src/renderer/env.d.ts b/packages/desktop-client/src/renderer/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/desktop-client/src/renderer/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/desktop-client/src/renderer/layouts/ClientLayout.vue b/packages/desktop-client/src/renderer/layouts/ClientLayout.vue new file mode 100644 index 0000000..f408376 --- /dev/null +++ b/packages/desktop-client/src/renderer/layouts/ClientLayout.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/packages/desktop-client/src/renderer/main.ts b/packages/desktop-client/src/renderer/main.ts new file mode 100644 index 0000000..b01cccc --- /dev/null +++ b/packages/desktop-client/src/renderer/main.ts @@ -0,0 +1,29 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import ElementPlus from 'element-plus'; +import zhCn from 'element-plus/es/locale/lang/zh-cn'; +import * as ElementPlusIconsVue from '@element-plus/icons-vue'; +import 'element-plus/dist/index.css'; + +import App from './App.vue'; +import router from './router/index.js'; +import { registerSmokeHooks } from './cache/self-test.js'; +import './styles/index.css'; + +const app = createApp(App); + +for (const [name, component] of Object.entries(ElementPlusIconsVue)) { + app.component(name, component); +} + +app.use(createPinia()); +app.use(router); +app.use(ElementPlus, { locale: zhCn }); + +// 冒烟验证模式:把缓存自检钩子暴露给主进程调用(正常运行时不存在) +if (window.desktop?.smokeTest) { + registerSmokeHooks(); +} + +await router.isReady(); +app.mount('#app'); diff --git a/packages/desktop-client/src/renderer/router/index.ts b/packages/desktop-client/src/renderer/router/index.ts new file mode 100644 index 0000000..24797ec --- /dev/null +++ b/packages/desktop-client/src/renderer/router/index.ts @@ -0,0 +1,70 @@ +import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'; +import { useAuthStore } from '../stores/auth.js'; + +/** + * Electron 生产环境通过 file:// 加载页面,无法使用 history 模式, + * 因此统一使用 hash 路由(开发与打包行为一致)。 + */ +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'login', + component: () => import('../views/LoginView.vue'), + meta: { public: true, title: '登录' }, + }, + { + path: '/', + component: () => import('../layouts/ClientLayout.vue'), + redirect: '/schedule', + children: [ + { + path: 'schedule', + name: 'schedule', + component: () => import('../views/ScheduleView.vue'), + meta: { title: '课表' }, + }, + { + path: 'homeworks', + name: 'homeworks', + component: () => import('../views/HomeworkView.vue'), + meta: { title: '作业' }, + }, + { + path: 'notifications', + name: 'notifications', + component: () => import('../views/NotificationView.vue'), + meta: { title: '通知' }, + }, + { + path: 'grades', + name: 'grades', + component: () => import('../views/GradeView.vue'), + meta: { title: '成绩' }, + }, + { + path: 'settings', + name: 'settings', + component: () => import('../views/SettingsView.vue'), + meta: { title: '设置' }, + }, + ], + }, + { path: '/:pathMatch(.*)*', redirect: '/schedule' }, +]; + +export const router = createRouter({ + history: createWebHashHistory(), + routes, +}); + +router.beforeEach((to) => { + const auth = useAuthStore(); + if (to.meta.public) { + if (to.name === 'login' && auth.isAuthenticated) return { name: 'schedule' }; + return true; + } + if (!auth.isAuthenticated) return { name: 'login' }; + return true; +}); + +export default router; diff --git a/packages/desktop-client/src/renderer/stores/app.ts b/packages/desktop-client/src/renderer/stores/app.ts new file mode 100644 index 0000000..bf44e47 --- /dev/null +++ b/packages/desktop-client/src/renderer/stores/app.ts @@ -0,0 +1,132 @@ +import { computed, ref } from 'vue'; +import { defineStore } from 'pinia'; +import { STORAGE_KEYS, formatDate } from '@classhelper/shared'; +import { pingHealth, setApiBaseUrl, setReachabilityReporter } from '../api/http.js'; +import { cacheClearAll, cacheStats, type CacheStoreStat } from '../cache/db.js'; +import { DEFAULT_SERVER, normalizeServerUrl } from '../config.js'; + +type RecoveryHandler = () => void; + +/** + * 应用级状态:服务器地址、可达性、离线缓存统计、同步时间。 + * 「联网后自动同步」的触发点就在 ping() 里:不可达 -> 可达 时通知所有订阅者刷新数据。 + */ +export const useAppStore = defineStore('app', () => { + const serverUrl = ref(DEFAULT_SERVER); + const serverReachable = ref(false); + const lastSyncAt = ref(null); + const cacheStoreStats = ref([]); + const currentWeek = ref(1); + const maxWeek = ref(20); + const initialized = ref(false); + + const offline = computed(() => !serverReachable.value); + const lastSyncText = computed(() => (lastSyncAt.value ? formatDate(lastSyncAt.value, true) : '尚未同步')); + const cacheItemCount = computed(() => cacheStoreStats.value.reduce((sum, item) => sum + item.count, 0)); + + const recoveryHandlers = new Set(); + let healthTimer: ReturnType | null = null; + + function onServerRecovered(handler: RecoveryHandler): void { + recoveryHandlers.add(handler); + } + + function offServerRecovered(handler: RecoveryHandler): void { + recoveryHandlers.delete(handler); + } + + /** 请求拦截器会把每次请求的结果回报过来,这里据此维护可达性 */ + setReachabilityReporter((reachable) => { + if (reachable && !serverReachable.value && initialized.value) { + serverReachable.value = true; + notifyRecovered(); + return; + } + serverReachable.value = reachable; + }); + + function notifyRecovered(): void { + for (const handler of recoveryHandlers) handler(); + } + + async function ping(): Promise { + const result = await pingHealth(); + if (result.ok) { + if (!serverReachable.value && initialized.value) notifyRecovered(); + serverReachable.value = true; + } else { + serverReachable.value = false; + } + } + + function startHealthPolling(): void { + if (healthTimer) return; + healthTimer = setInterval(() => { + void ping(); + }, 20000); + } + + function stopHealthPolling(): void { + if (!healthTimer) return; + clearInterval(healthTimer); + healthTimer = null; + } + + async function applyServerUrl(next: string): Promise { + const normalized = normalizeServerUrl(next); + if (!normalized) return false; + serverUrl.value = normalized; + setApiBaseUrl(normalized); + localStorage.setItem(STORAGE_KEYS.serverUrl, normalized); + if (window.desktop) await window.desktop.saveConfig({ serverUrl: normalized }); + return true; + } + + async function refreshCacheStats(): Promise { + try { + cacheStoreStats.value = await cacheStats(); + } catch { + cacheStoreStats.value = []; + } + } + + async function clearCache(): Promise { + await cacheClearAll(); + await refreshCacheStats(); + } + + async function init(serverFromConfig: string): Promise { + if (serverFromConfig) await applyServerUrl(serverFromConfig); + await refreshCacheStats(); + await ping(); + startHealthPolling(); + initialized.value = true; + } + + function markSynced(): void { + lastSyncAt.value = Date.now(); + void refreshCacheStats(); + } + + return { + serverUrl, + serverReachable, + offline, + lastSyncAt, + lastSyncText, + cacheStoreStats, + cacheItemCount, + currentWeek, + maxWeek, + initialized, + init, + ping, + applyServerUrl, + refreshCacheStats, + clearCache, + markSynced, + onServerRecovered, + offServerRecovered, + stopHealthPolling, + }; +}); diff --git a/packages/desktop-client/src/renderer/stores/auth.ts b/packages/desktop-client/src/renderer/stores/auth.ts new file mode 100644 index 0000000..e99069e --- /dev/null +++ b/packages/desktop-client/src/renderer/stores/auth.ts @@ -0,0 +1,131 @@ +import { computed, ref } from 'vue'; +import { defineStore } from 'pinia'; +import { STORAGE_KEYS, type LoginResponse, type StudentDto } from '@classhelper/shared'; +import { authApi } from '../api/index.js'; +import { setApiBaseUrl, setTokenProvider } from '../api/http.js'; +import { cacheGet, cacheSet } from '../cache/db.js'; +import { normalizeServerUrl } from '../config.js'; + +const PROFILE_KEY = 'me'; + +/** + * 登录态。 + * token 通过 IPC 交给主进程用 safeStorage 加密落盘,刷新/重启后可直接离线进入; + * 用户资料同时写入 IndexedDB,断网时也能显示姓名与班级。 + */ +export const useAuthStore = defineStore('auth', () => { + const token = ref(null); + const user = ref(null); + const loading = ref(false); + const offlineSession = ref(false); + + const isAuthenticated = computed(() => Boolean(token.value)); + const displayName = computed(() => user.value?.name ?? '未登录'); + const classId = computed(() => user.value?.classId ?? null); + + setTokenProvider(() => token.value); + + function applyToken(next: string | null): void { + token.value = next; + setTokenProvider(() => token.value); + } + + async function persistSession( + serverUrl: string, + username: string, + nextToken: string | null, + ): Promise { + if (!window.desktop) return; + await window.desktop.saveConfig({ serverUrl, username, token: nextToken }); + } + + /** 登录:写主进程配置 + 缓存资料 */ + async function login(serverUrl: string, username: string, password: string): Promise { + const normalized = normalizeServerUrl(serverUrl); + if (!normalized) throw new Error('服务器地址格式不正确,例如 http://127.0.0.1:4000'); + + setApiBaseUrl(normalized); + loading.value = true; + try { + const payload: LoginResponse = await authApi.login({ username, password }); + applyToken(payload.token); + user.value = payload.user as StudentDto; + offlineSession.value = false; + await cacheSet('profile', PROFILE_KEY, user.value); + await persistSession(normalized, username, payload.token); + } finally { + loading.value = false; + } + } + + /** + * 启动恢复:先用缓存资料立即渲染,再尝试向服务器刷新。 + * 服务器不可达时保持离线会话(可查看缓存数据)。 + */ + async function restore(): Promise<{ serverUrl: string; username: string }> { + let serverUrl = ''; + let username = ''; + if (window.desktop) { + const config = await window.desktop.getConfig(); + serverUrl = config.serverUrl; + username = config.username; + applyToken(config.token); + } + if (serverUrl) setApiBaseUrl(serverUrl); + if (!serverUrl) { + const fallback = localStorage.getItem(STORAGE_KEYS.serverUrl); + if (fallback) { + serverUrl = fallback; + setApiBaseUrl(fallback); + } + } + + const cachedProfile = await cacheGet('profile', PROFILE_KEY).catch(() => null); + if (cachedProfile) user.value = cachedProfile.value; + + // 服务器刷新放到后台,绝不阻塞启动:离线时也能立刻进入界面看缓存数据 + if (token.value) void refreshProfile(); + + return { serverUrl, username }; + } + + /** 后台刷新个人资料;服务器不可达时保持缓存资料与离线会话标记 */ + async function refreshProfile(): Promise { + if (!token.value) return; + try { + const profile = await authApi.me(); + user.value = profile; + offlineSession.value = false; + await cacheSet('profile', PROFILE_KEY, profile); + } catch { + offlineSession.value = Boolean(user.value); + } + } + + async function logout(): Promise { + try { + if (token.value) await authApi.logout(); + } catch { + // 离线退出忽略错误 + } + applyToken(null); + user.value = null; + offlineSession.value = false; + if (window.desktop) await window.desktop.saveConfig({ token: null }); + } + + return { + token, + user, + loading, + offlineSession, + isAuthenticated, + displayName, + classId, + login, + restore, + refreshProfile, + logout, + applyToken, + }; +}); diff --git a/packages/desktop-client/src/renderer/stores/notifications.ts b/packages/desktop-client/src/renderer/stores/notifications.ts new file mode 100644 index 0000000..89e7406 --- /dev/null +++ b/packages/desktop-client/src/renderer/stores/notifications.ts @@ -0,0 +1,107 @@ +import { computed, ref } from 'vue'; +import { defineStore } from 'pinia'; +import type { NotificationDto } from '@classhelper/shared'; +import { notificationApi } from '../api/index.js'; +import { cacheSet } from '../cache/db.js'; +import { fetchWithCache } from '../cache/index.js'; +import { useAppStore } from './app.js'; + +/** + * 通知状态:列表 + 未读数。 + * 抽成 store 是因为侧边栏红点与通知页共用同一份数据, + * 同时所有变更都会同步写回 IndexedDB,保证离线可见。 + */ +export const useNotificationStore = defineStore('notifications', () => { + const items = ref([]); + const unreadCount = ref(0); + const loading = ref(false); + const fromCache = ref(false); + const updatedAt = ref(null); + const error = ref(null); + + const unreadItems = computed(() => items.value.filter((item) => !item.read)); + + function currentKey(): string { + return 'self'; + } + + async function persist(): Promise { + await cacheSet('notifications', currentKey(), items.value).catch(() => undefined); + } + + function recount(): void { + unreadCount.value = items.value.filter((item) => !item.read).length; + } + + async function load(): Promise { + loading.value = true; + try { + const result = await fetchWithCache( + 'notifications', + currentKey(), + () => notificationApi.list(), + [], + ); + items.value = result.data; + fromCache.value = result.fromCache; + updatedAt.value = result.updatedAt; + error.value = result.error; + recount(); + if (!result.fromCache) useAppStore().markSynced(); + } finally { + loading.value = false; + } + } + + /** 实时推送:插入到列表头部并写回缓存 */ + async function pushRealtime(payload: NotificationDto): Promise { + if (items.value.some((item) => item.id === payload.id)) return; + items.value = [{ ...payload, read: false }, ...items.value]; + recount(); + await persist(); + } + + async function markRead(id: string): Promise { + await notificationApi.markRead(id); + const target = items.value.find((item) => item.id === id); + if (target) { + target.read = true; + target.readAt = new Date().toISOString(); + } + recount(); + await persist(); + } + + async function markAllRead(): Promise { + const result = await notificationApi.markAllRead(); + const now = new Date().toISOString(); + items.value = items.value.map((item) => ({ ...item, read: true, readAt: item.readAt ?? now })); + recount(); + await persist(); + return result.marked; + } + + async function refreshUnreadCount(): Promise { + try { + const result = await notificationApi.unreadCount(); + unreadCount.value = result.count; + } catch { + // 离线时保留缓存推算出的未读数 + } + } + + return { + items, + unreadItems, + unreadCount, + loading, + fromCache, + updatedAt, + error, + load, + pushRealtime, + markRead, + markAllRead, + refreshUnreadCount, + }; +}); diff --git a/packages/desktop-client/src/renderer/stores/realtime.ts b/packages/desktop-client/src/renderer/stores/realtime.ts new file mode 100644 index 0000000..454c27e --- /dev/null +++ b/packages/desktop-client/src/renderer/stores/realtime.ts @@ -0,0 +1,148 @@ +import { computed, ref } from 'vue'; +import { defineStore } from 'pinia'; +import { ElMessage } from 'element-plus'; +import { io, type Socket } from 'socket.io-client'; +import { + PRIORITY_LABELS, + SOCKET_EVENTS, + type GradeDto, + type HomeworkDto, + type NotificationDto, + type ServerToClientEvents, +} from '@classhelper/shared'; +import { socketUrlOf } from '../config.js'; + +type RealtimeEventName = keyof ServerToClientEvents; +type EventHandler = (payload: unknown) => void; + +/** + * 实时通道(学生端)。 + * 与 Web 管理端使用同一套事件名;收到事件后除了通知用户, + * 还会把数据写回 IndexedDB,保证断网时看到的是最后同步到的内容。 + */ +export const useRealtimeStore = defineStore('realtime', () => { + const connected = ref(false); + const connecting = ref(false); + const lastEventName = ref(null); + const lastEventAt = ref(null); + const eventCount = ref(0); + + const handlers = new Map>(); + let socket: Socket | null = null; + + const statusText = computed(() => { + if (connected.value) return '实时连接正常'; + if (connecting.value) return '正在连接服务器'; + return '实时连接已断开'; + }); + + function dispatch(event: string, payload: unknown): void { + for (const handler of handlers.get(event) ?? []) handler(payload); + } + + function mark(event: string): void { + lastEventName.value = event; + lastEventAt.value = Date.now(); + eventCount.value += 1; + } + + function bind(instance: Socket): void { + instance.on('connect', () => { + connected.value = true; + connecting.value = false; + dispatch('__connected__', null); + }); + instance.on('disconnect', () => { + connected.value = false; + dispatch('__disconnected__', null); + }); + instance.on('connect_error', (error: Error) => { + connected.value = false; + connecting.value = false; + dispatch('__connect_error__', error.message); + }); + + instance.on(SOCKET_EVENTS.notificationNew, (payload: NotificationDto) => { + mark(SOCKET_EVENTS.notificationNew); + ElMessage({ + message: `【${PRIORITY_LABELS[payload.priority] ?? payload.priority}】${payload.title}`, + type: payload.priority === 'URGENT' ? 'error' : 'info', + duration: 5000, + showClose: true, + }); + dispatch(SOCKET_EVENTS.notificationNew, payload); + }); + + instance.on(SOCKET_EVENTS.homeworkNew, (payload: HomeworkDto) => { + mark(SOCKET_EVENTS.homeworkNew); + ElMessage.success(`新作业:${payload.title}`); + dispatch(SOCKET_EVENTS.homeworkNew, payload); + }); + + instance.on(SOCKET_EVENTS.homeworkUpdated, (payload: HomeworkDto & { deleted?: boolean }) => { + mark(SOCKET_EVENTS.homeworkUpdated); + dispatch(SOCKET_EVENTS.homeworkUpdated, payload); + }); + + instance.on(SOCKET_EVENTS.gradeUpdated, (payload: GradeDto) => { + mark(SOCKET_EVENTS.gradeUpdated); + ElMessage.success(`成绩更新:${payload.examName} ${payload.score}/${payload.totalScore}`); + dispatch(SOCKET_EVENTS.gradeUpdated, payload); + }); + + instance.on(SOCKET_EVENTS.scheduleUpdated, (payload: unknown) => { + mark(SOCKET_EVENTS.scheduleUpdated); + dispatch(SOCKET_EVENTS.scheduleUpdated, payload); + }); + } + + function connect(serverUrl: string, token: string): void { + disconnect(); + connecting.value = true; + socket = io(socketUrlOf(serverUrl), { + auth: { token, clientType: 'desktop' }, + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionDelay: 1500, + reconnectionDelayMax: 10000, + }); + bind(socket); + } + + function disconnect(): void { + socket?.removeAllListeners(); + socket?.disconnect(); + socket = null; + connected.value = false; + connecting.value = false; + } + + function on( + event: RealtimeEventName | '__connected__' | '__disconnected__' | '__connect_error__', + handler: EventHandler, + ): void { + const set = handlers.get(event) ?? new Set(); + set.add(handler); + handlers.set(event, set); + } + + function off( + event: RealtimeEventName | '__connected__' | '__disconnected__' | '__connect_error__', + handler: EventHandler, + ): void { + handlers.get(event)?.delete(handler); + } + + return { + connected, + connecting, + statusText, + lastEventName, + lastEventAt, + eventCount, + connect, + disconnect, + on, + off, + }; +}); diff --git a/packages/desktop-client/src/renderer/styles/index.css b/packages/desktop-client/src/renderer/styles/index.css new file mode 100644 index 0000000..02902eb --- /dev/null +++ b/packages/desktop-client/src/renderer/styles/index.css @@ -0,0 +1,115 @@ +:root { + --ch-bg: #f5f7fa; + --ch-border: #e4e7ed; + --ch-sidebar: #1f2d3d; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +body { + font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + background: var(--ch-bg); + color: #303133; + /* 桌面端禁用整页选中,更接近原生应用体验 */ + user-select: none; +} + +input, +textarea { + user-select: text; +} + +.page { + padding: 16px 20px 24px; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.page-title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.page-subtitle { + margin: 4px 0 0; + font-size: 13px; + color: #909399; +} + +.toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.mt-12 { + margin-top: 12px; +} + +.mt-16 { + margin-top: 16px; +} + +.text-muted { + color: #909399; +} + +.offline-banner { + margin: 0 0 12px; +} + +.timetable-cell { + min-height: 30px; +} + +.timetable-course { + font-weight: 600; +} + +.timetable-meta { + font-size: 12px; + color: #909399; +} + +.unread-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #f56c6c; + margin-right: 6px; +} + +/* 滚动条美化(Windows 默认滚动条较粗) */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background: #c8ccd4; + border-radius: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} diff --git a/packages/desktop-client/src/renderer/views/GradeView.vue b/packages/desktop-client/src/renderer/views/GradeView.vue new file mode 100644 index 0000000..7dbcd58 --- /dev/null +++ b/packages/desktop-client/src/renderer/views/GradeView.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/packages/desktop-client/src/renderer/views/HomeworkView.vue b/packages/desktop-client/src/renderer/views/HomeworkView.vue new file mode 100644 index 0000000..6027ac7 --- /dev/null +++ b/packages/desktop-client/src/renderer/views/HomeworkView.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/packages/desktop-client/src/renderer/views/LoginView.vue b/packages/desktop-client/src/renderer/views/LoginView.vue new file mode 100644 index 0000000..ec847cf --- /dev/null +++ b/packages/desktop-client/src/renderer/views/LoginView.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/packages/desktop-client/src/renderer/views/NotificationView.vue b/packages/desktop-client/src/renderer/views/NotificationView.vue new file mode 100644 index 0000000..87edddf --- /dev/null +++ b/packages/desktop-client/src/renderer/views/NotificationView.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/packages/desktop-client/src/renderer/views/ScheduleView.vue b/packages/desktop-client/src/renderer/views/ScheduleView.vue new file mode 100644 index 0000000..22d606c --- /dev/null +++ b/packages/desktop-client/src/renderer/views/ScheduleView.vue @@ -0,0 +1,172 @@ + + + diff --git a/packages/desktop-client/src/renderer/views/SettingsView.vue b/packages/desktop-client/src/renderer/views/SettingsView.vue new file mode 100644 index 0000000..d76468c --- /dev/null +++ b/packages/desktop-client/src/renderer/views/SettingsView.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/packages/desktop-client/src/types/desktop.d.ts b/packages/desktop-client/src/types/desktop.d.ts new file mode 100644 index 0000000..b5c18e8 --- /dev/null +++ b/packages/desktop-client/src/types/desktop.d.ts @@ -0,0 +1,49 @@ +/** + * 主进程与渲染进程之间的桥接契约(preload 通过 contextBridge 暴露)。 + * 渲染进程只依赖这个最小 API,不接触 Node.js。 + */ + +export interface DesktopStoredConfig { + /** 后端服务地址,例如 http://127.0.0.1:4000 */ + serverUrl: string; + /** 上次登录的用户名(便于自动填充) */ + username: string; + /** 登录令牌(主进程用 safeStorage 加密后落盘,读回时自动解密) */ + token: string | null; +} + +export interface DesktopAppInfo { + appVersion: string; + electron: string; + chrome: string; + node: string; + platform: string; + userDataPath: string; + /** 是否处于冒烟验证模式(由 ELECTRON_SMOKE_TEST=1 触发) */ + smokeTest: boolean; +} + +export interface DesktopBridge { + /** 是否处于冒烟验证模式(由 ELECTRON_SMOKE_TEST=1 触发),同步可读 */ + smokeTest: boolean; + getConfig(): Promise; + saveConfig(patch: Partial): Promise; + clearConfig(): Promise; + getAppInfo(): Promise; + openExternal(url: string): Promise; +} + +declare global { + interface Window { + /** 仅在 Electron 中注入;浏览器里为 undefined(渲染进程做了降级处理) */ + desktop?: DesktopBridge; + /** 冒烟验证钩子,仅在 ELECTRON_SMOKE_TEST=1 时由渲染进程注册 */ + __classhelperSmoke__?: { + cacheSelfTest(): Promise<{ ok: boolean; detail: string }>; + offlineScenario(): Promise<{ ok: boolean; detail: string }>; + onlineScenario(): Promise<{ ok: boolean; detail: string }>; + }; + } +} + +export {}; diff --git a/packages/desktop-client/tsconfig.json b/packages/desktop-client/tsconfig.json new file mode 100644 index 0000000..064ec59 --- /dev/null +++ b/packages/desktop-client/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "jsx": "preserve", + "types": ["node"], + "baseUrl": ".", + "paths": { + "@renderer/*": ["./src/renderer/*"] + }, + "declaration": false, + "declarationMap": false + }, + "include": [ + "src/renderer/**/*.ts", + "src/renderer/**/*.d.ts", + "src/renderer/**/*.vue", + "src/types/**/*.d.ts" + ] +} diff --git a/packages/desktop-client/tsconfig.node.json b/packages/desktop-client/tsconfig.node.json new file mode 100644 index 0000000..e6789d4 --- /dev/null +++ b/packages/desktop-client/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "types": ["node"], + "declaration": false, + "declarationMap": false + }, + "include": ["src/main/**/*.ts", "src/preload/**/*.ts", "src/types/**/*.d.ts", "scripts/**/*.mjs"] +} diff --git a/packages/desktop-client/vite.config.mts b/packages/desktop-client/vite.config.mts new file mode 100644 index 0000000..9198868 --- /dev/null +++ b/packages/desktop-client/vite.config.mts @@ -0,0 +1,32 @@ +import { fileURLToPath, URL } from 'node:url'; +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +/** + * 渲染进程构建配置(.mts 以保证 Vite 原生配置加载器按 ESM 解析)。 + * + * 关键点: + * 1) base: './' —— Electron 通过 file:// 加载 index.html,必须使用相对路径 + * 2) outDir: dist/renderer —— 与主进程产物 dist/main、preload 产物 dist/preload 分离 + * 3) 端口 5174(Web 管理端占用 5173),dev 时主进程读取 VITE_DEV_SERVER_URL + */ +export default defineConfig({ + base: './', + plugins: [vue()], + resolve: { + alias: { + '@renderer': fileURLToPath(new URL('./src/renderer', import.meta.url)), + }, + }, + build: { + outDir: 'dist/renderer', + emptyOutDir: true, + sourcemap: false, + chunkSizeWarningLimit: 2000, + }, + server: { + host: '127.0.0.1', + port: 5174, + strictPort: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24b826b..af34cae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,61 @@ importers: specifier: ^10.4.1 version: 10.4.1(eslint@10.10.0(jiti@2.7.0)) + packages/desktop-client: + dependencies: + '@classhelper/shared': + specifier: workspace:* + version: link:../shared + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.42(typescript@5.9.3)) + axios: + specifier: ^1.20.0 + version: 1.20.0 + echarts: + specifier: ^6.1.0 + version: 6.1.0 + element-plus: + specifier: ^2.14.5 + version: 2.14.5(vue@3.5.42(typescript@5.9.3)) + pinia: + specifier: ^4.0.3 + version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.42(typescript@5.9.3)) + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + vue: + specifier: ^3.5.42 + version: 3.5.42(typescript@5.9.3) + vue-router: + specifier: ^5.3.1 + version: 5.3.1(@vue/compiler-sfc@3.5.42)(esbuild@0.28.2)(pinia@4.0.3(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.42(typescript@5.9.3)))(rolldown@1.2.8)(vite@8.3.0(@types/node@22.20.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))(vue@3.5.42(typescript@5.9.3)) + devDependencies: + '@types/node': + specifier: ^22.20.2 + version: 22.20.2 + '@vitejs/plugin-vue': + specifier: ^6.0.8 + version: 6.0.8(vite@8.3.0(@types/node@22.20.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))(vue@3.5.42(typescript@5.9.3)) + electron: + specifier: ^44.3.0 + version: 44.3.0 + electron-builder: + specifier: ^26.15.3 + version: 26.15.3(electron-builder-squirrel-windows@26.15.3) + esbuild: + specifier: ^0.28.2 + version: 0.28.2 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^8.3.0 + version: 8.3.0(@types/node@22.20.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13) + vue-tsc: + specifier: ^3.3.11 + version: 3.3.11(typescript@5.9.3) + packages/server: dependencies: '@classhelper/shared': @@ -186,6 +241,50 @@ packages: '@electric-sql/pglite@0.4.3': resolution: {integrity: sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==} + '@electron-internal/extract-zip@1.0.5': + resolution: {integrity: sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==} + engines: {node: '>=22.12.0'} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@3.1.0': + resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==} + engines: {node: '>=14'} + + '@electron/get@5.1.0': + resolution: {integrity: sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==} + engines: {node: '>=22.12.0'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@4.2.0': + resolution: {integrity: sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@electron/universal@2.0.3': + resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} + engines: {node: '>=16.4'} + + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} + hasBin: true + '@element-plus/icons-vue@2.3.2': resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} peerDependencies: @@ -406,6 +505,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -488,12 +591,43 @@ packages: cpu: [x64] os: [win32] + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + '@oxc-project/types@0.149.0': resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} + '@peculiar/asn1-schema@2.9.4': + resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} + engines: {node: '>=14'} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/webcrypto@1.7.1': + resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} + engines: {node: '>=14.18.0'} + '@prisma/adapter-libsql@7.10.0': resolution: {integrity: sha512-ANjUjJ/TrDQhR8jFU8HvYWRdwP17Lox1WL81zZvHcPVeFehZfNomAsPMNY24EBTLLZp0Xw4Na5Zf1S+EuKMKxw==} @@ -730,6 +864,10 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -739,9 +877,16 @@ packages: '@sxzz/popperjs-es@2.11.8': resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -781,6 +926,9 @@ packages: '@types/d3-time@3.0.0': resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -793,9 +941,15 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -805,6 +959,9 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} @@ -817,6 +974,9 @@ packages: '@types/node@22.20.2': resolution: {integrity: sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==} + '@types/node@24.13.4': + resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==} + '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -826,6 +986,9 @@ packages: '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -1009,6 +1172,14 @@ packages: peerDependencies: vue: ^3.5.0 + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} + engines: {node: '>=10.0.0'} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1031,6 +1202,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -1040,14 +1215,36 @@ packages: alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.3.0: resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + app-builder-lib@26.15.3: + resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.15.3 + electron-builder-squirrel-windows: 26.15.3 + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + ast-kit@2.2.0: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} @@ -1056,26 +1253,46 @@ packages: resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} engines: {node: '>=20.19.0'} + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} async-validator@4.2.5: resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + axios@1.20.0: resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} @@ -1090,6 +1307,9 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -1097,6 +1317,16 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + brace-expansion@5.0.9: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} @@ -1104,10 +1334,25 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + builder-util-runtime@9.7.0: + resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} + engines: {node: '>=12.0.0'} + + builder-util@26.15.3: + resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} + engines: {node: '>=14.0.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -1116,6 +1361,14 @@ packages: magicast: optional: true + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + cacheable@2.5.0: resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} @@ -1127,6 +1380,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1135,17 +1392,61 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@10.0.5: resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==} engines: {node: '>=22'} @@ -1180,10 +1481,16 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1256,6 +1563,10 @@ packages: supports-color: optional: true + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1263,6 +1574,18 @@ packages: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -1292,6 +1615,23 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@26.15.3: + resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -1300,6 +1640,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -1312,6 +1655,31 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@26.15.3: + resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==} + + electron-builder@26.15.3: + resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@26.15.3: + resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} + + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + + electron@44.3.0: + resolution: {integrity: sha512-St9EV7F2VtYaYWD2qaAjBwUgKxx39eJOUsUJ5+/1113sqbVfNqv4Dbm/W1rN7qmYSPa+mWwR6yr+b7MfgjgVfQ==} + engines: {node: '>= 22.12.0'} + hasBin: true + element-plus@2.14.5: resolution: {integrity: sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==} peerDependencies: @@ -1323,6 +1691,9 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1331,6 +1702,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + engine.io-client@6.6.6: resolution: {integrity: sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==} @@ -1346,10 +1720,17 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1366,6 +1747,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -1445,6 +1829,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -1486,6 +1873,9 @@ packages: file-entry-cache@11.1.5: resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -1529,6 +1919,33 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1559,6 +1976,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + giget@3.3.1: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} hasBin: true @@ -1567,10 +1988,26 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1580,6 +2017,13 @@ packages: graphmatch@1.1.1: resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1605,14 +2049,33 @@ packages: hookified@2.2.0: resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -1629,6 +2092,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1644,6 +2111,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1654,9 +2125,33 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1664,6 +2159,13 @@ packages: js-base64@3.9.3: resolution: {integrity: sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1673,6 +2175,20 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -1683,9 +2199,15 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1814,6 +2336,14 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lru.min@1.1.5: resolution: {integrity: sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} @@ -1825,6 +2355,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1856,10 +2390,49 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -1893,6 +2466,30 @@ packages: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} + node-abi@4.35.0: + resolution: {integrity: sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==} + engines: {node: '>=22.12.0'} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + normalize-wheel-es@1.2.0: resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} @@ -1910,6 +2507,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + ohash@2.0.12: resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} @@ -1924,6 +2525,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1943,6 +2548,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1953,6 +2562,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} @@ -1979,6 +2592,14 @@ packages: pkg-types@2.3.3: resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + pkijs@3.4.0: + resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} + engines: {node: '>=16.0.0'} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + postcss-selector-parser@7.1.6: resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} @@ -1991,6 +2612,11 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2013,9 +2639,24 @@ packages: typescript: optional: true + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + promise-limit@2.7.0: resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -2027,6 +2668,9 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2034,6 +2678,13 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.2.0: + resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==} + engines: {node: '>=16.0.0'} + qified@0.10.1: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} @@ -2045,6 +2696,10 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + range-parser@1.3.0: resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} @@ -2065,6 +2720,13 @@ packages: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} @@ -2072,10 +2734,24 @@ packages: remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -2084,6 +2760,15 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -2099,6 +2784,9 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2109,12 +2797,35 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -2127,6 +2838,10 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -2169,6 +2884,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + socket.io-adapter@2.5.8: resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} @@ -2188,10 +2907,24 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2199,22 +2932,62 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -2223,6 +2996,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -2244,6 +3020,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} @@ -2266,6 +3046,25 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@6.28.1: + resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} + engines: {node: '>=18.17'} + + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} + engines: {node: '>=20.18.1'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2307,9 +3106,15 @@ packages: webpack: optional: true + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2412,6 +3217,9 @@ packages: typescript: optional: true + webcrypto-core@1.9.2: + resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2420,10 +3228,24 @@ packages: engines: {node: '>= 8'} hasBin: true + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -2447,6 +3269,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} @@ -2455,10 +3281,25 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yargs@18.0.0: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -2515,6 +3356,100 @@ snapshots: '@electric-sql/pglite@0.4.3': {} + '@electron-internal/extract-zip@1.0.5': {} + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + + '@electron/get@3.1.0': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/get@5.1.0': + dependencies: + debug: 4.4.3 + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.8.5 + sumchecker: 3.0.1 + optionalDependencies: + undici: 7.29.1 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@4.2.0': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + node-abi: 4.35.0 + node-api-version: 0.2.1 + node-gyp: 12.4.0 + read-binary-file-arch: 1.0.6 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.3': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + dir-compare: 4.2.0 + fs-extra: 11.4.0 + minimatch: 9.0.9 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.3 + fs-extra: 11.4.0 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + '@element-plus/icons-vue@2.3.2(vue@3.5.42(typescript@5.9.3))': dependencies: vue: 3.5.42(typescript@5.9.3) @@ -2654,6 +3589,10 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -2739,10 +3678,49 @@ snapshots: '@libsql/win32-x64-msvc@0.5.29': optional: true + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + '@neon-rs/load@0.0.4': {} + '@noble/hashes@1.4.0': {} + + '@noble/hashes@2.4.0': {} + '@oxc-project/types@0.149.0': {} + '@peculiar/asn1-schema@2.9.4': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + + '@peculiar/webcrypto@1.7.1': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + tslib: 2.8.1 + webcrypto-core: 1.9.2 + '@prisma/adapter-libsql@7.10.0': dependencies: '@libsql/client': 0.17.4 @@ -2950,17 +3928,30 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sindresorhus/is@4.6.0': {} + '@socket.io/component-emitter@3.1.2': {} '@standard-schema/spec@1.1.0': {} '@sxzz/popperjs-es@2.11.8': {} + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 '@types/node': 22.20.2 + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.20.2 + '@types/responselike': 1.0.3 + '@types/connect@3.4.38': dependencies: '@types/node': 22.20.2 @@ -2999,6 +3990,10 @@ snapshots: '@types/d3-time@3.0.0': {} + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -3016,8 +4011,14 @@ snapshots: '@types/express-serve-static-core': 5.1.3 '@types/serve-static': 2.2.0 + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.20.2 + '@types/geojson@7946.0.16': {} + '@types/http-cache-semantics@4.2.0': {} + '@types/http-errors@2.0.5': {} '@types/json-schema@7.0.15': {} @@ -3027,6 +4028,10 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 22.20.2 + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.20.2 + '@types/lodash-es@4.17.12': dependencies: '@types/lodash': 4.17.25 @@ -3039,6 +4044,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.13.4': + dependencies: + undici-types: 7.18.2 + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} @@ -3047,6 +4056,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.20.2 + '@types/send@1.2.1': dependencies: '@types/node': 22.20.2 @@ -3348,6 +4361,10 @@ snapshots: dependencies: vue: 3.5.42(typescript@5.9.3) + '@xmldom/xmldom@0.8.15': {} + + abbrev@4.0.0: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -3370,6 +4387,8 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3386,10 +4405,72 @@ snapshots: alien-signals@3.2.1: {} + ansi-regex@5.0.1: {} + ansi-regex@6.3.0: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@6.2.3: {} + app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3): + dependencies: + '@electron/asar': 3.4.1 + '@electron/fuses': 1.8.0 + '@electron/get': 3.1.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.3 + '@electron/rebuild': 4.2.0 + '@electron/universal': 2.0.3 + '@malept/flatpak-bundler': 0.4.0 + '@noble/hashes': 2.4.0 + '@peculiar/webcrypto': 1.7.1 + '@types/fs-extra': 9.0.13 + ajv: 8.20.0 + asn1js: 3.0.10 + async-exit-hook: 2.0.1 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chromium-pickle-js: 0.2.0 + ci-info: 4.3.1 + debug: 4.4.3 + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.15.3) + electron-publish: 26.15.3 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + isbinaryfile: 5.0.7 + jiti: 2.7.0 + js-yaml: 4.3.2 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.6 + pkijs: 3.4.0 + plist: 3.1.0 + proper-lockfile: 4.1.2 + resedit: 1.7.2 + semver: 7.7.4 + tar: 7.5.22 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + unzipper: 0.12.5 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + + argparse@2.0.1: {} + + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + ast-kit@2.2.0: dependencies: '@babel/parser': 7.29.8 @@ -3401,16 +4482,24 @@ snapshots: '@babel/types': 7.29.8 ast-kit: 2.2.0 + async-exit-hook@2.0.1: {} + async-mutex@0.5.0: dependencies: tslib: 2.8.1 async-validator@4.2.5: {} + async@3.2.6: {} + asynckit@0.4.0: {} + at-least-node@1.0.0: {} + aws-ssl-profiles@1.1.2: {} + aws4@1.13.2: {} + axios@1.20.0: dependencies: follow-redirects: 1.16.0 @@ -3421,8 +4510,12 @@ snapshots: - debug - supports-color + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} + base64id@2.0.0: {} bcryptjs@3.0.3: {} @@ -3431,6 +4524,8 @@ snapshots: birpc@2.9.0: {} + bluebird@3.7.2: {} + body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -3447,14 +4542,56 @@ snapshots: boolbase@1.0.0: {} + boolean@3.2.0: + optional: true + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + + builder-util-runtime@9.7.0: + dependencies: + debug: 4.4.3 + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + + builder-util@26.15.3: + dependencies: + '@types/debug': 4.1.13 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + js-yaml: 4.3.2 + sanitize-filename: 1.6.4 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + bytes@3.1.2: {} + bytestreamjs@2.0.1: {} + c12@3.3.4: dependencies: chokidar: 5.0.0 @@ -3470,6 +4607,18 @@ snapshots: pkg-types: 2.3.3 rc9: 3.1.0 + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + cacheable@2.5.0: dependencies: '@cacheable/memory': 2.2.0 @@ -3488,24 +4637,62 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@5.6.2: {} chokidar@5.0.0: dependencies: readdirp: 5.1.1 + chownr@3.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@4.3.1: {} + + ci-info@4.4.0: {} + classnames@2.5.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@9.0.1: dependencies: string-width: 7.2.0 strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@5.1.0: {} + + commander@9.5.0: + optional: true + + compare-version@0.1.2: {} + + concat-map@0.0.1: {} + concurrently@10.0.5: dependencies: chalk: 5.6.2 @@ -3531,11 +4718,16 @@ snapshots: cookie@0.7.2: {} + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 + cross-dirname@0.1.0: + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3598,10 +4790,30 @@ snapshots: dependencies: ms: 2.1.3 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-is@0.1.4: {} deepmerge-ts@7.1.5: {} + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + defu@6.1.7: {} delaunator@5.1.0: @@ -3620,6 +4832,30 @@ snapshots: detect-libc@2.1.2: {} + detect-node@2.1.0: + optional: true + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + fs-extra: 10.1.0 + js-yaml: 4.3.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + dotenv@17.4.2: {} dunder-proto@1.0.1: @@ -3628,6 +4864,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -3644,6 +4884,69 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@26.15.3(dmg-builder@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + ci-info: 4.4.0 + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + fs-extra: 10.1.0 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.3 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-publish@26.15.3: + dependencies: + '@types/fs-extra': 9.0.13 + aws4: 1.13.2 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + form-data: 4.0.6 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.3 + fs-extra: 7.0.1 + lodash: 4.18.1 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2 + transitivePeerDependencies: + - supports-color + + electron@44.3.0: + dependencies: + '@electron-internal/extract-zip': 1.0.5 + '@electron/get': 5.1.0 + '@types/node': 24.13.4 + transitivePeerDependencies: + - supports-color + element-plus@2.14.5(vue@3.5.42(typescript@5.9.3)): dependencies: '@ctrl/tinycolor': 4.2.1 @@ -3667,10 +4970,16 @@ snapshots: emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} + empathic@2.0.0: {} encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + engine.io-client@6.6.6: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -3703,8 +5012,12 @@ snapshots: entities@7.0.1: {} + env-paths@2.2.1: {} + env-paths@3.0.0: {} + err-code@2.0.3: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -3720,6 +5033,9 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + es6-error@4.1.1: + optional: true + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -3838,6 +5154,8 @@ snapshots: etag@1.8.1: {} + exponential-backoff@3.1.3: {} + express@5.2.1: dependencies: accepts: 2.0.0 @@ -3899,6 +5217,10 @@ snapshots: dependencies: flat-cache: 6.1.23 + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -3948,6 +5270,45 @@ snapshots: fresh@2.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -3981,20 +5342,70 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + giget@3.3.1: {} glob-parent@6.0.2: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.8.5 + serialize-error: 7.0.1 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + graceful-fs@4.2.11: {} grammex@3.1.13: {} graphmatch@1.1.1: {} + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -4015,6 +5426,12 @@ snapshots: hookified@2.2.0: {} + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + http-cache-semantics@4.2.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -4023,6 +5440,18 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -4030,6 +5459,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -4040,6 +5476,11 @@ snapshots: imurmurhash@0.1.4: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} internmap@2.0.3: {} @@ -4048,6 +5489,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4056,18 +5499,55 @@ snapshots: is-property@1.0.2: {} + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + isexe@2.0.0: {} + isexe@3.1.5: {} + + isexe@4.0.0: {} + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + jiti@2.7.0: {} js-base64@3.9.3: {} + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -4092,10 +5572,16 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 + lazy-val@1.0.5: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4201,6 +5687,12 @@ snapshots: long@5.3.2: {} + lowercase-keys@2.0.0: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lru.min@1.1.5: {} magic-string-ast@1.0.3: @@ -4211,6 +5703,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + math-intrinsics@1.1.0: {} media-typer@1.1.1: {} @@ -4231,10 +5728,40 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@2.6.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mlly@1.8.2: dependencies: acorn: 8.18.0 @@ -4272,6 +5799,35 @@ snapshots: dependencies: content-type: 2.1.0 + node-abi@4.35.0: + dependencies: + semver: 7.8.5 + + node-api-version@0.2.1: + dependencies: + semver: 7.8.5 + + node-gyp@12.4.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.5 + tar: 7.5.22 + tinyglobby: 0.2.17 + undici: 6.28.1 + which: 6.0.1 + + node-int64@0.4.0: {} + + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + + normalize-url@6.1.0: {} + normalize-wheel-es@1.2.0: {} nostics@1.2.0: {} @@ -4284,6 +5840,9 @@ snapshots: object-inspect@1.13.4: {} + object-keys@1.1.1: + optional: true + ohash@2.0.12: {} on-finished@2.4.1: @@ -4303,6 +5862,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + p-cancelable@2.1.1: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4317,12 +5878,16 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-to-regexp@8.4.2: {} pathe@2.0.3: {} + pe-library@0.4.1: {} + perfect-debounce@2.1.0: {} picocolors@1.1.1: {} @@ -4349,6 +5914,21 @@ snapshots: exsolve: 1.1.1 pathe: 2.0.3 + pkijs@3.4.0: + dependencies: + '@noble/hashes': 1.4.0 + asn1js: 3.0.10 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.15 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 @@ -4362,6 +5942,11 @@ snapshots: postgres@3.4.7: {} + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + prelude-ls@1.2.1: {} prettier@3.9.6: {} @@ -4383,8 +5968,19 @@ snapshots: - react - react-dom + proc-log@6.1.0: {} + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + promise-limit@2.7.0: {} + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -4398,10 +5994,21 @@ snapshots: proxy-from-env@2.1.0: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@6.1.0: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.2.0: {} + qified@0.10.1: dependencies: hookified: 2.2.0 @@ -4413,6 +6020,8 @@ snapshots: quansync@0.2.11: {} + quick-lru@5.1.1: {} + range-parser@1.3.0: {} raw-body@3.0.2: @@ -4434,16 +6043,58 @@ snapshots: react@19.3.0: {} + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@5.1.1: {} remeda@2.33.4: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + ret@0.5.0: {} retry@0.12.0: {} + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + robust-predicates@3.0.3: {} rolldown@1.2.8: @@ -4481,6 +6132,8 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-regex2@5.1.1: @@ -4489,10 +6142,25 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.6.1: {} + scheduler@0.28.0: {} scule@1.3.0: {} + semver-compare@1.0.0: + optional: true + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + semver@7.8.5: {} send@1.2.1: @@ -4513,6 +6181,11 @@ snapshots: seq-queue@0.0.5: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -4564,6 +6237,10 @@ snapshots: signal-exit@4.1.0: {} + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + socket.io-adapter@2.5.8: dependencies: debug: 4.4.3 @@ -4607,33 +6284,101 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.1.3: + optional: true + sqlstring@2.3.3: {} + stat-mode@1.0.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.3.0 + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + supports-color@10.2.2: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.7 + + tmp@0.2.7: {} + toidentifier@1.0.1: {} tree-kill@1.2.2: {} + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -4652,6 +6397,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.13.1: + optional: true + type-is@2.1.0: dependencies: content-type: 2.1.0 @@ -4675,6 +6423,17 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.18.2: {} + + undici@6.28.1: {} + + undici@7.29.1: + optional: true + + universalify@0.1.2: {} + + universalify@2.0.1: {} + unpipe@1.0.0: {} unplugin-utils@0.3.2: @@ -4692,10 +6451,20 @@ snapshots: rolldown: 1.2.8 vite: 8.3.0(@types/node@22.20.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13) + unzipper@0.12.5: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.1 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + uri-js@4.4.1: dependencies: punycode: 2.3.1 + utf8-byte-length@1.0.5: {} + util-deprecate@1.0.2: {} valibot@1.4.2(typescript@5.9.3): @@ -4783,14 +6552,36 @@ snapshots: optionalDependencies: typescript: 5.9.3 + webcrypto-core@1.9.2: + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + webpack-virtual-modules@0.6.2: {} which@2.0.2: dependencies: isexe: 2.0.0 + which@5.0.0: + dependencies: + isexe: 3.1.5 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -4803,12 +6594,30 @@ snapshots: xml-name-validator@5.0.0: {} + xmlbuilder@15.1.1: {} + xmlhttprequest-ssl@2.1.2: {} y18n@5.0.8: {} + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yargs@18.0.0: dependencies: cliui: 9.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8692328..3310d5e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,9 @@ allowBuilds: vue-demi: true '@parcel/watcher': true unrs-resolver: true + # electron-builder 的可选依赖(Squirrel.Windows 打包器)。本项目只用 nsis / portable, + # 显式置为 false,避免 pnpm 11 因「未声明允许或禁止」而中断整次安装。 + electron-winstaller: false # 上游新版本发布后立即安装可能带来风险,这里保留默认解析行为。 # 若要锁定 Node 版本或开启更严格的 peer 校验,可在此追加: