commit f8c8c688a14fa201ee338aaf7178ab56fedf033b Author: richarjiang Date: Wed Jul 15 17:12:04 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8e34e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +out/ +.DS_Store +*.log +.env +.env.* +!.env.example diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2a41a15 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# Tennis Book 开发约定 + +## 产品边界 + +- 本项目是桌面端网球场聚合预约工具,不在渲染层直接调用第三方微信小程序接口。 +- 每个球场必须通过独立适配器接入;公共层只定义球场、日期、场地和时段等稳定概念。 +- 不在公共模型中硬塞某个球场的专属字段。专属请求参数、签名、Cookie 和流程状态留在对应适配器内。 + +## 目录结构 + +- `src/main/`:Electron 主进程、第三方网络请求、凭证和适配器注册。 +- `src/main/settings/`:按球场保存的本机配置与原子写入逻辑,存储位置使用 Electron `userData` 目录。 +- `src/main/bookings/`:待支付锁场订单摘要与恢复逻辑;不得持久化支付签名。 +- `src/main/adapters//`:单个球场的接口与预约流程实现。 +- `src/preload/`:仅暴露经过白名单审核的类型安全 IPC。 +- `src/renderer/`:React UI,不可使用 Node.js API,不可持有第三方密钥。 +- `src/shared/`:主进程、预加载和渲染层共享的纯类型与消息契约。 +- `docs/`:架构决策和球场接入说明。 + +## 命名与扩展规则 + +- 球场 ID 使用稳定的 `kebab-case`,适配器目录名必须与 ID 一致。 +- IPC channel 使用 `domain:action`,新增 channel 时必须同时更新共享类型和 preload 白名单。 +- 新球场先实现 `CourtAdapter`,再注册到适配器注册表;禁止在 React 组件里按球场 ID 写业务分支。 +- 第三方接口字段 `userId` 在本项目语义中是“场地 ID”;固定场地 ID 属于球场适配器私有常量,不得通过 IPC 或配置界面暴露。 +- 场地 ID 只允许在对应适配器请求边界映射到第三方 `userId` 字段和 `STOREID` 请求头。 +- 下单、取消等写操作必须由对应球场适配器独立实现;公共层只编排确认、状态和恢复,不拼装球场专属请求。 +- 模拟数据只允许存在于 `demo` 适配器或明确命名的 fixture 中,接入真实 API 后不得静默回退到模拟成功。 + +## 安全规则 + +- Electron 必须保持 `contextIsolation: true`、`nodeIntegration: false` 和 `sandbox: true`。 +- 第三方令牌、Cookie、签名密钥不得进入渲染进程、日志或版本库。 +- 第三方请求诊断日志只允许在 dev 模式输出;请求头和响应中的访客凭证、支付签名、预支付标识及个人信息必须递归脱敏。 +- `PSPLVISITORID` 持久化在 Electron 主进程管理的本机配置中;设置弹窗可通过白名单 IPC 读取原值用于回填,但关闭弹窗后必须清除渲染层状态,其他业务接口不得返回原值。 +- 每球场配置以 `courtId` 隔离;标识符在共享模型和持久化文件中使用字符串,实际请求前再按接口契约安全转换。 +- 预约人昵称和手机号仅可按产品明确要求存在于当次时段查询的内存结果中;不得写日志、落盘、缓存或用于预约以外的用途。 +- 外部链接只能通过主进程白名单打开;页面不得任意导航或创建窗口。 +- 写操作前必须在主进程重新验证实时可用性和价格;UI 必须二次确认。不得用真实接口做自动化下单或取消测试。 + +## 完成标准 + +- 每次修改至少通过 `npm run typecheck`、`npm run build` 和 `git diff --check`。 +- 影响交互或布局时必须做一次实际渲染检查。 +- 接入真实球场时需要覆盖:正常数据、无可用时段、登录失效、限流、接口格式变化和网络失败。 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..c7e3d95 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,51 @@ +# 架构说明 + +## 为什么选 Electron + +本项目的主要复杂度不是绘制桌面窗口,而是长期维护多个非统一的微信小程序接口:不同签名方式、Cookie、请求头、代理、登录状态和多步骤预约流程。Electron 主进程可以直接复用 Node.js 网络生态,并与 Chromium 渲染层保持清晰隔离,能以最低的适配成本覆盖这些差异。 + +Tauri 的安装包更小、空闲内存通常更低,但它会把接口适配层拆成 Rust 命令或 sidecar。当前业务没有强约束要求最小包体,却明确要求持续定制多个 JavaScript/HTTP 流程,因此 Electron 的开发与排障成本更低。 + +## 运行边界 + +```text +React renderer + | typed window.tennisBook API +Preload allowlist + | ipcRenderer.invoke +Electron main process + | AdapterRegistry + CourtSettingsStore +CourtAdapter A / CourtAdapter B / ... + | HTTP, cookies, signatures, booking workflow +微信小程序后端 API +``` + +渲染层只知道统一的 `CourtSummary` 和 `AvailabilityDay`。某个球场的原始响应、鉴权和预约步骤不能穿过 IPC 边界。 + +`AvailabilityDay` 可包含界面明确需要展示的预约人昵称和手机号,但适配器必须从第三方原始响应中逐项提取,不能把原始预约对象整体穿过 IPC;这些信息只保留在当前查询的内存状态中,不记录、不缓存、不落盘。 + +preload 必须构建为 CommonJS `.js`。项目启用了 Electron renderer sandbox,而 sandboxed preload 没有 ESM 上下文;不能通过关闭沙箱来迁就 `.mjs` 构建产物。 + +## 每球场运行时配置 + +`PSPLVISITORID` 等运行时凭证按 `courtId` 独立存储在 Electron `userData` 目录。范思伯特福中福的场地 ID 固定为适配器私有常量,只在请求边界映射到第三方 `userId` 字段和 `STOREID` 请求头,不经过 IPC,也不在配置界面暴露。设置弹窗通过专用白名单 IPC 读取凭证原值并回填密码框,默认遮挡;关闭弹窗立即清除渲染层中的输入值和设置对象,其他查询及预约接口不返回凭证。 + +配置文件使用临时文件加重命名的方式原子替换。共享层始终用字符串表达外部 ID,避免超出 JavaScript 安全整数范围;具体适配器负责校验接口是否接受该范围并转换为请求格式。 + +## 接入一个新球场 + +1. 在 `src/main/adapters//` 创建适配器。 +2. 实现 `CourtAdapter` 的 `court` 与 `getAvailability()`。 +3. 在适配器内部完成原始响应到共享模型的转换。 +4. 将适配器加入 `src/main/adapters/registry.ts`。 +5. 为登录失效、空数据和接口字段变化添加契约测试。 + +## 预约写操作 + +公共预约状态机为:`可订 -> 二次确认 -> 重新查询校验 -> 结果待确认记录 -> 下单锁场 -> 待支付 -> 取消释放/支付完成`。具体请求体、版本头、访客凭证和响应解析只存在于球场适配器中。所有写操作按球场在主进程串行化,待支付或结果不确定期间禁止修改该球场凭证。 + +下单前由主进程重新查询同一天的数据,并使用重新查询得到的场地 UID、时间和价格构造请求,拒绝已订、排课和活动报名时段。渲染层传入的价格只作为用户确认金额;如与最新价格不同则拒绝下单并要求重新确认。第三方 UID 不接受渲染层输入。 + +范思伯特福中福的 `SaveVenueAppointmentV2` 只创建待支付订单并锁场,不代表支付成功。支付脚本不经过 IPC、不记录日志、不落盘;待支付订单的非敏感摘要按球场持久化,以便应用重启后仍能恢复“取消释放”入口。POST 发送前先持久化结果待确认记录,网络超时或响应解析失败时保留“尝试释放”入口,避免生成不可恢复的孤儿锁场。 + +`CrmAutoReleaseVenueAppoint` 使用当前球场配置与空 JSON 请求体释放当前访客的锁场,并不能绑定订单号。发送释放请求前先持久化 `release-uncertain`;若响应超时或进程中断,系统禁止自动重试,用户必须在小程序核实后显式清理本机记录。远端确认释放后先持久化 `release-confirmed` 标记,再删除摘要,避免清理失败后重复调用全局释放接口。支付脚本的过期时间只用于展示,不作为场地锁已经释放的依据。 diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..cf75944 --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,31 @@ +import { resolve } from 'node:path' +import react from '@vitejs/plugin-react' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + resolve: { + alias: { + '@shared': resolve('src/shared') + } + } + }, + preload: { + plugins: [externalizeDepsPlugin()], + resolve: { + alias: { + '@shared': resolve('src/shared') + } + } + }, + renderer: { + resolve: { + alias: { + '@renderer': resolve('src/renderer/src'), + '@shared': resolve('src/shared') + } + }, + plugins: [react()] + } +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..74687a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3361 @@ +{ + "name": "tennis-book", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tennis-book", + "version": "0.1.0", + "dependencies": { + "@fontsource-variable/manrope": "5.2.8", + "@fontsource-variable/newsreader": "5.2.8", + "lucide-react": "0.468.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@swc/core": "1.15.18", + "@types/node": "22.15.3", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.5", + "@vitejs/plugin-react": "4.7.0", + "electron": "35.7.5", + "electron-vite": "3.1.0", + "typescript": "5.9.3", + "vite": "6.4.3", + "vitest": "3.2.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://mirrors.tencent.com/npm/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://mirrors.tencent.com/npm/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource-variable/manrope": { + "version": "5.2.8", + "resolved": "https://mirrors.tencent.com/npm/@fontsource-variable/manrope/-/manrope-5.2.8.tgz", + "integrity": "sha512-nc9lOuCRz73UHnovDE2bwXUdghE2SEOc7Aii0qGe3CLyE03W1a7VnY5Z6euRiapiKbCkGS+eXbY3s/kvWeGeSw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/newsreader": { + "version": "5.2.8", + "resolved": "https://mirrors.tencent.com/npm/@fontsource-variable/newsreader/-/newsreader-5.2.8.tgz", + "integrity": "sha512-Ivo7tZPqQZ/erBbjx3mc+PvI0w+TuUhy93zlvIu3dupq2AlSF328sbkAjYyhwE91v8qpiJ88ogQB684hdiPtmw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://mirrors.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://mirrors.tencent.com/npm/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://mirrors.tencent.com/npm/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://mirrors.tencent.com/npm/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://mirrors.tencent.com/npm/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://mirrors.tencent.com/npm/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://mirrors.tencent.com/npm/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://mirrors.tencent.com/npm/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://mirrors.tencent.com/npm/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://mirrors.tencent.com/npm/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://mirrors.tencent.com/npm/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://mirrors.tencent.com/npm/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://mirrors.tencent.com/npm/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://mirrors.tencent.com/npm/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://mirrors.tencent.com/npm/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://mirrors.tencent.com/npm/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.3", + "resolved": "https://mirrors.tencent.com/npm/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://mirrors.tencent.com/npm/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.18", + "resolved": "https://mirrors.tencent.com/npm/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.5", + "resolved": "https://mirrors.tencent.com/npm/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://mirrors.tencent.com/npm/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://mirrors.tencent.com/npm/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://mirrors.tencent.com/npm/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://mirrors.tencent.com/npm/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://mirrors.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://mirrors.tencent.com/npm/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://mirrors.tencent.com/npm/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://mirrors.tencent.com/npm/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://mirrors.tencent.com/npm/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://mirrors.tencent.com/npm/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://mirrors.tencent.com/npm/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://mirrors.tencent.com/npm/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://mirrors.tencent.com/npm/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://mirrors.tencent.com/npm/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://mirrors.tencent.com/npm/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://mirrors.tencent.com/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://mirrors.tencent.com/npm/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://mirrors.tencent.com/npm/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://mirrors.tencent.com/npm/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://mirrors.tencent.com/npm/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://mirrors.tencent.com/npm/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://mirrors.tencent.com/npm/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron": { + "version": "35.7.5", + "resolved": "https://mirrors.tencent.com/npm/electron/-/electron-35.7.5.tgz", + "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", + "dev": true + }, + "node_modules/electron-vite": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/electron-vite/-/electron-vite-3.1.0.tgz", + "integrity": "sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "cac": "^6.7.14", + "esbuild": "^0.25.1", + "magic-string": "^0.30.17", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://mirrors.tencent.com/npm/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://mirrors.tencent.com/npm/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://mirrors.tencent.com/npm/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://mirrors.tencent.com/npm/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://mirrors.tencent.com/npm/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://mirrors.tencent.com/npm/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://mirrors.tencent.com/npm/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://mirrors.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://mirrors.tencent.com/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://mirrors.tencent.com/npm/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://mirrors.tencent.com/npm/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://mirrors.tencent.com/npm/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://mirrors.tencent.com/npm/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://mirrors.tencent.com/npm/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://mirrors.tencent.com/npm/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://mirrors.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://mirrors.tencent.com/npm/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://mirrors.tencent.com/npm/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://mirrors.tencent.com/npm/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://mirrors.tencent.com/npm/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://mirrors.tencent.com/npm/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://mirrors.tencent.com/npm/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://mirrors.tencent.com/npm/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://mirrors.tencent.com/npm/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://mirrors.tencent.com/npm/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://mirrors.tencent.com/npm/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://mirrors.tencent.com/npm/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://mirrors.tencent.com/npm/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://mirrors.tencent.com/npm/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://mirrors.tencent.com/npm/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://mirrors.tencent.com/npm/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://mirrors.tencent.com/npm/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://mirrors.tencent.com/npm/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://mirrors.tencent.com/npm/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://mirrors.tencent.com/npm/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://mirrors.tencent.com/npm/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://mirrors.tencent.com/npm/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://mirrors.tencent.com/npm/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://mirrors.tencent.com/npm/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://mirrors.tencent.com/npm/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://mirrors.tencent.com/npm/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://mirrors.tencent.com/npm/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://mirrors.tencent.com/npm/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://mirrors.tencent.com/npm/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://mirrors.tencent.com/npm/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://mirrors.tencent.com/npm/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://mirrors.tencent.com/npm/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://mirrors.tencent.com/npm/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://mirrors.tencent.com/npm/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://mirrors.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://mirrors.tencent.com/npm/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://mirrors.tencent.com/npm/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://mirrors.tencent.com/npm/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://mirrors.tencent.com/npm/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://mirrors.tencent.com/npm/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://mirrors.tencent.com/npm/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://mirrors.tencent.com/npm/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://mirrors.tencent.com/npm/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://mirrors.tencent.com/npm/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://mirrors.tencent.com/npm/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://mirrors.tencent.com/npm/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://mirrors.tencent.com/npm/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://mirrors.tencent.com/npm/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://mirrors.tencent.com/npm/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://mirrors.tencent.com/npm/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://mirrors.tencent.com/npm/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://mirrors.tencent.com/npm/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://mirrors.tencent.com/npm/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://mirrors.tencent.com/npm/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://mirrors.tencent.com/npm/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://mirrors.tencent.com/npm/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://mirrors.tencent.com/npm/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://mirrors.tencent.com/npm/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://mirrors.tencent.com/npm/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://mirrors.tencent.com/npm/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://mirrors.tencent.com/npm/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://mirrors.tencent.com/npm/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://mirrors.tencent.com/npm/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://mirrors.tencent.com/npm/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://mirrors.tencent.com/npm/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://mirrors.tencent.com/npm/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://mirrors.tencent.com/npm/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://mirrors.tencent.com/npm/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://mirrors.tencent.com/npm/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://mirrors.tencent.com/npm/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://mirrors.tencent.com/npm/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://mirrors.tencent.com/npm/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://mirrors.tencent.com/npm/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ab5d0c5 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "tennis-book", + "version": "0.1.0", + "private": true, + "description": "Desktop workspace for aggregated tennis court booking", + "main": "./out/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "build": "npm run typecheck && electron-vite build", + "preview": "electron-vite preview", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@fontsource-variable/manrope": "5.2.8", + "@fontsource-variable/newsreader": "5.2.8", + "lucide-react": "0.468.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@swc/core": "1.15.18", + "@types/node": "22.15.3", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.5", + "@vitejs/plugin-react": "4.7.0", + "electron": "35.7.5", + "electron-vite": "3.1.0", + "typescript": "5.9.3", + "vite": "6.4.3", + "vitest": "3.2.7" + } +} diff --git a/src/main/adapters/fansibote-fuzhong/adapter.test.ts b/src/main/adapters/fansibote-fuzhong/adapter.test.ts new file mode 100644 index 0000000..6c57ffe --- /dev/null +++ b/src/main/adapters/fansibote-fuzhong/adapter.test.ts @@ -0,0 +1,297 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { AvailabilityQuery } from '../../../shared/contracts' +import { + buildAvailabilityRequestBody, + buildCreateBookingRequestBody, + fansiboteFuzhongAdapter, + parseCreateBookingResponse, + parseAvailabilityResponse +} from './adapter' + +const query: AvailabilityQuery = { + courtId: 'fansibote-fuzhong', + date: '2026-07-16' +} + +function createSlot(status: number, canApptOrNot: boolean) { + return { + classroomUid: 1775556146897330236, + txtClassroomUid: '1775556146897330236', + classRoomName: '1号风雨场', + beginDatetime: '2026-07-16 08:00:00', + endDatetime: '2026-07-16 08:59:00', + cost: 80, + status, + apptInfo: { + canApptOrNot, + customerNames: ['王小球'], + hadVenueAppts: [{ + customerName: '王小球', + customerTel: '13800000000' + }] + } + } +} + +describe('范思伯特福中福响应映射', () => { + afterEach(() => vi.unstubAllGlobals()) + + it('把内部场地 ID 映射为第三方 userId 字段', () => { + const body = buildAvailabilityRequestBody('2026-07-16') + + expect(body).toEqual({ + dateTime: '2026-07-16', + userId: 6038652, + projectUid: '1775556129325750199' + }) + }) + + it('列表请求固定使用适配器私有的场地 ID', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ successed: true, result: { slots: [] } }) + }) + vi.stubGlobal('fetch', fetchMock) + + await fansiboteFuzhongAdapter.getAvailability(query, { + courtId: query.courtId + }) + + expect(fetchMock).toHaveBeenCalledOnce() + const request = fetchMock.mock.calls[0]?.[1] as RequestInit + expect(request.headers).toEqual(expect.objectContaining({ + STOREID: '6038652' + })) + expect(JSON.parse(String(request.body))).toEqual({ + dateTime: '2026-07-16', + userId: 6038652, + projectUid: '1775556129325750199' + }) + }) + + it('使用实时重新查询得到的场地引用和价格构造下单请求', () => { + const slot = parseAvailabilityResponse({ + successed: true, + result: { slots: [createSlot(0, true)] } + }, query).slots[0]! + + expect(buildCreateBookingRequestBody(slot)).toEqual({ + userId: 6038652, + projectType: 0, + classroomItems: [{ + classroomUid: '1775556146897330236', + beginDatetime: '2026-07-16 08:00:00', + endDatetime: '2026-07-16 08:59:00', + peopleNum: 1 + }], + remark: '', + combinationPayments: [{ paymentMethod: 900, cost: 80 }] + }) + }) + + it('把下单成功响应映射为待支付锁场,不暴露支付脚本给公共订单', () => { + const slot = parseAvailabilityResponse({ + successed: true, + result: { slots: [createSlot(0, true)] } + }, query).slots[0]! + const result = parseCreateBookingResponse({ + successed: true, + result: { + apptUid: '1784102970857723008', + script: { + expireTime: '2026-07-15 16:24:31', + paySign: 'private-pay-sign' + }, + venueAppointUids: ['1784102970857723008'] + } + }, slot, query.courtId) + + expect(result.booking).toEqual({ + courtId: query.courtId, + orderId: '1784102970857723008', + venueAppointmentIds: ['1784102970857723008'], + amountCents: 8000, + courtLabel: '1号风雨场', + startTime: '08:00', + endTime: '08:59', + expiresAt: '2026-07-15 16:24:31', + status: 'pending-payment' + }) + expect(JSON.stringify(result.booking)).not.toContain('private-pay-sign') + }) + + it('下单和取消都注入访客凭证,取消请求体严格为空对象', async () => { + const slot = parseAvailabilityResponse({ + successed: true, + result: { slots: [createSlot(0, true)] } + }, query).slots[0]! + const fetchMock = vi.fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + successed: true, + result: { + apptUid: 'order-1', + script: { expireTime: '2026-07-15 16:24:31' }, + venueAppointUids: ['order-1'] + } + }) + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ successed: true }) + }) + vi.stubGlobal('fetch', fetchMock) + const settings = { + courtId: query.courtId, + visitorId: 'visitor-token' + } + + await fansiboteFuzhongAdapter.createBooking!(slot, settings) + await fansiboteFuzhongAdapter.cancelBooking!(settings) + + const createRequest = fetchMock.mock.calls[0]?.[1] as RequestInit + const cancelRequest = fetchMock.mock.calls[1]?.[1] as RequestInit + expect(createRequest.headers).toEqual(expect.objectContaining({ + PSPLVISITORID: 'visitor-token', + STOREID: '6038652', + VERSIONINFO: 'NC|2026.07.13', + xweb_xhr: '1', + 'Sec-Fetch-Site': 'cross-site', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + Referer: 'https://servicewechat.com/wx2088ca46b47d8be7/12/page-frame.html', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf2641b36) XWEB/25193' + })) + expect(cancelRequest.headers).toEqual(expect.objectContaining({ + PSPLVISITORID: 'visitor-token', + STOREID: '6038652', + VERSIONINFO: 'NC|2026.07.13', + Referer: 'https://servicewechat.com/wx2088ca46b47d8be7/12/page-frame.html' + })) + expect(cancelRequest.body).toBe('{}') + }) + + it('未配置访客凭证时拒绝产生下单请求', async () => { + const slot = parseAvailabilityResponse({ + successed: true, + result: { slots: [createSlot(0, true)] } + }, query).slots[0]! + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + + await expect(fansiboteFuzhongAdapter.createBooking!(slot, { + courtId: query.courtId + })).rejects.toThrow('PSPLVISITORID') + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('保留字符串 UID、价格和时间,可订时段不携带预约人信息', () => { + const result = parseAvailabilityResponse( + { + successed: true, + result: { slots: [createSlot(0, true)] } + }, + query + ) + + expect(result.slots).toEqual([ + expect.objectContaining({ + id: '1775556146897330236:2026-07-16 08:00:00', + courtLabel: '1号风雨场', + startTime: '08:00', + endTime: '08:59', + priceCents: 8000, + status: 'available' + }) + ]) + expect(JSON.stringify(result)).not.toContain('13800000000') + expect(JSON.stringify(result)).not.toContain('王小球') + }) + + it('区分已预约与排课', () => { + const result = parseAvailabilityResponse( + { + successed: true, + result: { + slots: [createSlot(1, false), createSlot(4, false)] + } + }, + query + ) + + expect(result.slots.map(({ status, unavailableLabel, bookedBy }) => ({ + status, + unavailableLabel, + bookedBy + }))).toEqual([ + { + status: 'booked', + unavailableLabel: '已订', + bookedBy: [{ name: '王小球', phone: '13800000000' }] + }, + { status: 'blocked', unavailableLabel: '排课', bookedBy: undefined } + ]) + }) + + it('把跨小时活动合并到同一子场的每个对应时段', () => { + const firstSlot = createSlot(0, true) + const secondSlot = { + ...createSlot(0, true), + beginDatetime: '2026-07-16 09:00:00', + endDatetime: '2026-07-16 09:59:00' + } + const result = parseAvailabilityResponse( + { + successed: true, + result: { + slots: [firstSlot, secondSlot], + enrollSlots: [{ + txtClassroomUid: '1775556146897330236', + beginDatetime: '2026-07-16 08:00:00', + endDatetime: '2026-07-16 09:59:00', + enrollmentRule: { + uidTxt: '1784027073124705081', + title: '【2.5双打】福中福8-10', + beginDate: '2026-07-16 08:00:00', + endDate: '2026-07-16 10:00:00', + enrollmentEndDate: '2026-07-16 07:00:00', + enrollmentFee: 70, + enrollmentCount: 3, + enrollmentLimitNum: 4, + customerModels: [{ name: '星' }, { name: '浩哥' }] + } + }] + } + }, + query + ) + + expect(result.slots.map((slot) => slot.enrollment)).toEqual([ + { + id: '1784027073124705081', + title: '【2.5双打】福中福8-10', + startTime: '08:00', + endTime: '10:00', + feeCents: 7000, + enrolledCount: 3, + capacity: 4, + participantNames: ['星', '浩哥'], + enrollmentDeadline: '07:00' + }, + expect.objectContaining({ id: '1784027073124705081' }) + ]) + }) + + it('接口字段变化时明确失败,不静默丢时段', () => { + expect(() => parseAvailabilityResponse( + { + successed: true, + result: { slots: [{ classRoomName: '字段缺失' }] } + }, + query + )).toThrow('无法识别的时段数据') + }) +}) diff --git a/src/main/adapters/fansibote-fuzhong/adapter.ts b/src/main/adapters/fansibote-fuzhong/adapter.ts new file mode 100644 index 0000000..b778bcc --- /dev/null +++ b/src/main/adapters/fansibote-fuzhong/adapter.ts @@ -0,0 +1,525 @@ +import { createHash } from 'node:crypto' +import type { + AvailabilityDay, + AvailabilityQuery, + BookingContact, + BookingSlot, + CourtSummary, + EnrollmentActivity +} from '../../../shared/contracts' +import type { CourtRuntimeSettings } from '../../settings/types' +import { ProviderMutationError, type AdapterBookingResult, type CourtAdapter } from '../types' + +const AVAILABILITY_ENDPOINT = + 'https://wxservice-stg48.pospal.cn/wxapi/AppointmentVenue/LoadValidClassRoomApptSettingV2' +const CREATE_BOOKING_ENDPOINT = + 'https://wxservice-stg48.pospal.cn/wxapi/AppointmentVenue/SaveVenueAppointmentV2' +const CANCEL_BOOKING_ENDPOINT = + 'https://wxservice-stg48.pospal.cn/wxapi/AppointmentVenue/CrmAutoReleaseVenueAppoint' +const DEFAULT_VENUE_ID = '6038652' +const PROJECT_UID = '1775556129325750199' +const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/ +const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} (\d{2}:\d{2}):\d{2}$/ +const WECHAT_USER_AGENT = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf2641b36) XWEB/25193' + +export const fansiboteFuzhongCourt: CourtSummary = { + id: 'fansibote-fuzhong', + name: '范思伯特福中福', + shortName: '范思', + accent: '#D8FF63', + status: 'online' +} + +const defaultSettings: CourtRuntimeSettings = { + courtId: fansiboteFuzhongCourt.id +} + +interface PospalSlot { + txtClassroomUid: unknown + classRoomName: unknown + beginDatetime: unknown + endDatetime: unknown + cost: unknown + status: unknown + apptInfo: unknown +} + +interface PospalResponse { + successed?: unknown + status?: unknown + errorCode?: unknown + result?: unknown +} + +interface ParsedEnrollment { + classroomUid: string + beginDatetime: string + endDatetime: string + activity: EnrollmentActivity +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +const SENSITIVE_LOG_KEY = /visitor|paysign|prepay|nonce|package|token|cookie|authorization|customer|phone|tel/i + +function redactForLog(value: unknown, key = ''): unknown { + if (SENSITIVE_LOG_KEY.test(key)) return '[REDACTED]' + if (Array.isArray(value)) return value.map((item) => redactForLog(item)) + if (isRecord(value)) { + return Object.fromEntries( + Object.entries(value).map(([entryKey, entryValue]) => [ + entryKey, + redactForLog(entryValue, entryKey) + ]) + ) + } + return value +} + +function logDevRequest(event: string, details: Record) { + if (!import.meta.env.DEV) return + console.info( + `[tennis-book][fansibote-fuzhong][${event}]`, + JSON.stringify(redactForLog(details), null, 2) + ) +} + +function credentialFingerprint(visitorId: string) { + return createHash('sha256').update(visitorId).digest('hex').slice(0, 12) +} + +function providerErrorDetail(payload: unknown): string | null { + if (!isRecord(payload)) return null + const keys = ['message', 'msg', 'errorMessage', 'errorMsg', 'status', 'errorCode'] + const details = keys.flatMap((key) => { + const value = payload[key] + return typeof value === 'string' || typeof value === 'number' + ? [`${key}=${String(value).slice(0, 200)}`] + : [] + }) + return details.length > 0 ? details.join(', ') : null +} + +function extractTime(value: unknown): string | null { + if (typeof value !== 'string') return null + return DATE_TIME_PATTERN.exec(value)?.[1] ?? null +} + +function parseBookingContacts(appointment: Record): BookingContact[] { + const venueAppointments = Array.isArray(appointment.hadVenueAppts) + ? appointment.hadVenueAppts + : [] + const contacts: BookingContact[] = [] + + for (const venueAppointment of venueAppointments) { + if (!isRecord(venueAppointment)) continue + const name = typeof venueAppointment.customerName === 'string' + ? venueAppointment.customerName.trim() + : '' + const phone = typeof venueAppointment.customerTel === 'string' + ? venueAppointment.customerTel.trim() + : '' + if (name || phone) contacts.push({ + name: name || undefined, + phone: phone || undefined + }) + } + + if (contacts.length === 0 && Array.isArray(appointment.customerNames)) { + for (const value of appointment.customerNames) { + if (typeof value === 'string' && value.trim()) { + contacts.push({ name: value.trim() }) + } + } + } + + return contacts.filter((contact, index) => + contacts.findIndex((candidate) => + candidate.name === contact.name && candidate.phone === contact.phone + ) === index + ) +} + +function parseParticipantNames(value: unknown): string[] { + if (!Array.isArray(value)) return [] + const names = value.flatMap((customer) => { + if (!isRecord(customer) || typeof customer.name !== 'string') return [] + const name = customer.name.trim() + return name ? [name] : [] + }) + return [...new Set(names)] +} + +function parseEnrollment(value: unknown): ParsedEnrollment { + if (!isRecord(value) || !isRecord(value.enrollmentRule)) { + throw new Error('球场接口返回了无法识别的活动数据') + } + + const rule = value.enrollmentRule + const startTime = extractTime(rule.beginDate) + const endTime = extractTime(rule.endDate) + const deadline = extractTime(rule.enrollmentEndDate) + const enrolledCount = typeof rule.enrollmentCount === 'number' + ? rule.enrollmentCount + : typeof rule.ParticipateCount === 'number' + ? rule.ParticipateCount + : 0 + + if ( + typeof value.txtClassroomUid !== 'string' || + typeof value.beginDatetime !== 'string' || + typeof value.endDatetime !== 'string' || + typeof rule.uidTxt !== 'string' || + typeof rule.title !== 'string' || + startTime === null || + endTime === null || + typeof rule.enrollmentFee !== 'number' || + !Number.isFinite(rule.enrollmentFee) || + typeof rule.enrollmentLimitNum !== 'number' || + !Number.isFinite(rule.enrollmentLimitNum) + ) { + throw new Error('球场接口返回了无法识别的活动数据') + } + + return { + classroomUid: value.txtClassroomUid, + beginDatetime: value.beginDatetime, + endDatetime: value.endDatetime, + activity: { + id: rule.uidTxt, + title: rule.title, + startTime, + endTime, + feeCents: Math.round(rule.enrollmentFee * 100), + enrolledCount, + capacity: rule.enrollmentLimitNum, + participantNames: parseParticipantNames(rule.customerModels), + enrollmentDeadline: deadline ?? undefined + } + } +} + +function parseSlot(value: unknown, enrollments: ParsedEnrollment[]): BookingSlot { + if (!isRecord(value)) throw new Error('球场接口返回了无法识别的时段数据') + + const slot = value as unknown as PospalSlot + const startTime = extractTime(slot.beginDatetime) + const endTime = extractTime(slot.endDatetime) + const appointment = isRecord(slot.apptInfo) ? slot.apptInfo : null + const canBook = appointment?.canApptOrNot + + if ( + typeof slot.txtClassroomUid !== 'string' || + typeof slot.classRoomName !== 'string' || + typeof slot.beginDatetime !== 'string' || + typeof slot.endDatetime !== 'string' || + startTime === null || + endTime === null || + typeof slot.cost !== 'number' || + !Number.isFinite(slot.cost) || + typeof slot.status !== 'number' || + typeof canBook !== 'boolean' + ) { + throw new Error('球场接口返回了无法识别的时段数据') + } + const slotBeginDatetime = slot.beginDatetime + const slotEndDatetime = slot.endDatetime + + const status: BookingSlot['status'] = canBook + ? 'available' + : slot.status === 1 + ? 'booked' + : 'blocked' + const bookedBy = status === 'booked' && appointment + ? parseBookingContacts(appointment) + : [] + const enrollment = enrollments.find((candidate) => + candidate.classroomUid === slot.txtClassroomUid && + slotBeginDatetime <= candidate.endDatetime && + slotEndDatetime >= candidate.beginDatetime + )?.activity + + return { + id: `${slot.txtClassroomUid}:${slotBeginDatetime}`, + courtLabel: slot.classRoomName, + startTime, + endTime, + priceCents: Math.round(slot.cost * 100), + status, + unavailableLabel: + status === 'booked' ? '已订' : status === 'blocked' ? '排课' : undefined, + bookedBy: bookedBy.length > 0 ? bookedBy : undefined, + enrollment, + providerReference: { + classroomUid: slot.txtClassroomUid, + beginDatetime: slotBeginDatetime, + endDatetime: slotEndDatetime + }, + sourceLabel: '范思伯特实时接口' + } +} + +export function parseAvailabilityResponse( + payload: unknown, + query: AvailabilityQuery +): AvailabilityDay { + if (!isRecord(payload)) throw new Error('球场接口响应格式错误') + + const response = payload as PospalResponse + if (response.successed !== true || !isRecord(response.result)) { + throw new Error('球场接口拒绝了可订时段查询') + } + + const slots = response.result.slots + if (!Array.isArray(slots)) throw new Error('球场接口响应缺少时段列表') + const enrollmentSlots = response.result.enrollSlots + if (enrollmentSlots !== undefined && !Array.isArray(enrollmentSlots)) { + throw new Error('球场接口响应中的活动列表格式错误') + } + const enrollments = (enrollmentSlots ?? []).map(parseEnrollment) + + return { + courtId: query.courtId, + date: query.date, + updatedAt: new Date().toISOString(), + slots: slots.map((slot) => parseSlot(slot, enrollments)) + } +} + +export function buildAvailabilityRequestBody(date: string) { + return { + dateTime: date, + userId: Number(DEFAULT_VENUE_ID), + projectUid: PROJECT_UID + } +} + +async function getAvailability( + query: AvailabilityQuery, + settings: CourtRuntimeSettings +): Promise { + if (query.courtId !== fansiboteFuzhongCourt.id) { + throw new Error('球场标识与适配器不匹配') + } + if (settings.courtId !== query.courtId) throw new Error('球场配置与适配器不匹配') + if (!DATE_PATTERN.test(query.date)) throw new Error('预约日期格式错误') + + const response = await fetch(AVAILABILITY_ENDPOINT, { + method: 'POST', + headers: { + Accept: '*/*', + APPTYPE: '1', + 'Content-Type': 'application/json', + PSPLVISITORAUTO: 'API', + Referer: 'https://servicewechat.com/wx2088ca46b47d8be7/11/page-frame.html', + STOREID: DEFAULT_VENUE_ID, + VERSIONINFO: 'NC|2026.07.03' + }, + body: JSON.stringify(buildAvailabilityRequestBody(query.date)), + signal: AbortSignal.timeout(12_000) + }) + + if (!response.ok) { + throw new Error(`球场接口请求失败(HTTP ${response.status})`) + } + + return parseAvailabilityResponse(await response.json(), query) +} + +function requireVisitorId(settings: CourtRuntimeSettings): string { + const visitorId = settings.visitorId?.trim() + if (!visitorId) throw new Error('请先在球场配置中填写 PSPLVISITORID') + return visitorId +} + +function createWriteHeaders(settings: CourtRuntimeSettings) { + return { + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'zh-CN,zh;q=0.9', + APPTYPE: '1', + 'Content-Type': 'application/json', + PSPLVISITORID: requireVisitorId(settings), + Referer: 'https://servicewechat.com/wx2088ca46b47d8be7/12/page-frame.html', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'cross-site', + STOREID: DEFAULT_VENUE_ID, + 'User-Agent': WECHAT_USER_AGENT, + VERSIONINFO: 'NC|2026.07.13', + xweb_xhr: '1' + } +} + +export function buildCreateBookingRequestBody( + slot: BookingSlot +) { + if (!slot.providerReference) throw new Error('时段缺少下单引用信息') + + return { + userId: Number(DEFAULT_VENUE_ID), + projectType: 0, + classroomItems: [{ + classroomUid: slot.providerReference.classroomUid, + beginDatetime: slot.providerReference.beginDatetime, + endDatetime: slot.providerReference.endDatetime, + peopleNum: 1 + }], + remark: '', + combinationPayments: [{ + paymentMethod: 900, + cost: slot.priceCents / 100 + }] + } +} + +export function parseCreateBookingResponse( + payload: unknown, + slot: BookingSlot, + courtId: string +): AdapterBookingResult { + if (!isRecord(payload)) { + throw new ProviderMutationError('球场下单接口响应格式错误', 'uncertain') + } + if (payload.successed !== true) { + const detail = providerErrorDetail(payload) + throw new ProviderMutationError( + `球场下单接口拒绝了锁场请求${detail ? `(${detail})` : ''}`, + 'rejected' + ) + } + if (!isRecord(payload.result)) { + throw new ProviderMutationError('球场下单接口响应格式错误', 'uncertain') + } + const result = payload.result + if ( + typeof result.apptUid !== 'string' || + !isRecord(result.script) || + typeof result.script.expireTime !== 'string' || + !Array.isArray(result.venueAppointUids) || + !result.venueAppointUids.every((value) => typeof value === 'string') + ) { + throw new ProviderMutationError('球场下单接口响应格式错误', 'uncertain') + } + + return { + booking: { + courtId, + orderId: result.apptUid, + venueAppointmentIds: result.venueAppointUids, + amountCents: slot.priceCents, + courtLabel: slot.courtLabel, + startTime: slot.startTime, + endTime: slot.endTime, + expiresAt: result.script.expireTime, + status: 'pending-payment' + }, + paymentScript: result.script + } +} + +async function createBooking( + slot: BookingSlot, + settings: CourtRuntimeSettings +): Promise { + if (slot.status !== 'available' && slot.status !== 'limited') { + throw new Error('该时段当前不可预约') + } + if (slot.enrollment) throw new Error('活动时段不能通过普通场地接口下单') + const headers = createWriteHeaders(settings) + const requestBody = buildCreateBookingRequestBody(slot) + logDevRequest('booking.request', { + url: CREATE_BOOKING_ENDPOINT, + method: 'POST', + credentialFingerprint: credentialFingerprint(headers.PSPLVISITORID), + headers, + body: requestBody + }) + + let response: Response + try { + response = await fetch(CREATE_BOOKING_ENDPOINT, { + method: 'POST', + headers, + body: JSON.stringify(requestBody), + signal: AbortSignal.timeout(12_000) + }) + } catch (error) { + logDevRequest('booking.network-error', { + message: error instanceof Error ? error.message : String(error) + }) + throw new ProviderMutationError('球场下单网络响应不确定', 'uncertain') + } + let payload: unknown + try { + payload = await response.json() + } catch { + logDevRequest('booking.response', { + httpStatus: response.status, + ok: response.ok, + body: '[UNPARSEABLE]' + }) + if (!response.ok) { + throw new ProviderMutationError( + `球场下单请求被拒绝(HTTP ${response.status})`, + 'rejected' + ) + } + throw new ProviderMutationError('球场下单响应无法解析', 'uncertain') + } + logDevRequest('booking.response', { + httpStatus: response.status, + ok: response.ok, + body: payload + }) + if (!response.ok) { + const detail = providerErrorDetail(payload) + throw new ProviderMutationError( + `球场下单请求被拒绝(HTTP ${response.status}${detail ? `,${detail}` : ''})`, + 'rejected' + ) + } + return parseCreateBookingResponse(payload, slot, settings.courtId) +} + +async function cancelBooking(settings: CourtRuntimeSettings): Promise { + const headers = createWriteHeaders(settings) + let response: Response + try { + response = await fetch(CANCEL_BOOKING_ENDPOINT, { + method: 'POST', + headers, + body: '{}', + signal: AbortSignal.timeout(12_000) + }) + } catch { + throw new ProviderMutationError('取消锁场网络响应不确定', 'uncertain') + } + if (!response.ok) { + throw new ProviderMutationError( + `取消锁场请求被拒绝(HTTP ${response.status})`, + 'rejected' + ) + } + let payload: unknown + try { + payload = await response.json() + } catch { + throw new ProviderMutationError('取消锁场响应无法解析', 'uncertain') + } + if (!isRecord(payload) || payload.successed !== true) { + throw new ProviderMutationError('取消接口拒绝释放场地', 'rejected') + } +} + +export const fansiboteFuzhongAdapter: CourtAdapter = { + court: fansiboteFuzhongCourt, + defaultSettings, + getAvailability, + preflightWriteSettings: requireVisitorId, + createBooking, + cancelBooking +} diff --git a/src/main/adapters/registry.test.ts b/src/main/adapters/registry.test.ts new file mode 100644 index 0000000..73a1652 --- /dev/null +++ b/src/main/adapters/registry.test.ts @@ -0,0 +1,316 @@ +import { mkdtemp } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { CourtSettingsStore } from '../settings/CourtSettingsStore' +import { PendingBookingStore } from '../bookings/PendingBookingStore' +import { AdapterRegistry } from './registry' + +const courtId = 'fansibote-fuzhong' +const slotId = '1775556146897330236:2026-07-16 08:00:00' + +function availabilityPayload() { + return { + successed: true, + result: { + slots: [{ + txtClassroomUid: '1775556146897330236', + classRoomName: '1号风雨场', + beginDatetime: '2026-07-16 08:00:00', + endDatetime: '2026-07-16 08:59:00', + cost: 80, + status: 0, + apptInfo: { canApptOrNot: true } + }] + } + } +} + +describe('AdapterRegistry 预约状态机', () => { + afterEach(() => vi.unstubAllGlobals()) + + it('重新校验后锁场,阻止重复订单,并在取消成功后释放状态', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-registry-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ + courtId, + visitorId: 'visitor-token' + }) + const pendingStore = new PendingBookingStore(join(directory, 'pending.json')) + const registry = new AdapterRegistry(settingsStore, pendingStore) + const fetchMock = vi.fn() + .mockResolvedValueOnce({ ok: true, json: async () => availabilityPayload() }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + successed: true, + result: { + apptUid: 'order-1', + script: { expireTime: '2026-07-16 08:10:00' }, + venueAppointUids: ['order-1'] + } + }) + }) + .mockResolvedValueOnce({ ok: true, json: async () => ({ successed: true }) }) + vi.stubGlobal('fetch', fetchMock) + + const booking = await registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + }) + expect(booking.orderId).toBe('order-1') + await expect(registry.getPendingBooking(courtId)).resolves.toEqual(booking) + expect(fetchMock).toHaveBeenCalledTimes(2) + + await expect(registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + })).rejects.toThrow('已有待支付订单') + expect(fetchMock).toHaveBeenCalledTimes(2) + + await expect(registry.updateCourtSettings({ + courtId, + visitorId: 'replacement-token' + })).rejects.toThrow('请先取消释放后再修改配置') + + await registry.cancelBooking({ courtId, orderId: 'order-1' }) + expect(fetchMock).toHaveBeenCalledTimes(3) + await expect(registry.getPendingBooking(courtId)).resolves.toBeNull() + }) + + it('在主进程串行化并发下单,只允许生成一个锁场订单', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-concurrent-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'visitor-token' }) + const registry = new AdapterRegistry( + settingsStore, + new PendingBookingStore(join(directory, 'pending.json')) + ) + const fetchMock = vi.fn() + .mockResolvedValueOnce({ ok: true, json: async () => availabilityPayload() }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + successed: true, + result: { + apptUid: 'order-1', + script: { expireTime: '2026-07-16 08:10:00' }, + venueAppointUids: ['order-1'] + } + }) + }) + vi.stubGlobal('fetch', fetchMock) + + const input = { courtId, date: '2026-07-16', slotId, expectedPriceCents: 8000 } + const results = await Promise.allSettled([ + registry.createBooking(input), + registry.createBooking(input) + ]) + + expect(results.filter((result) => result.status === 'fulfilled')).toHaveLength(1) + expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1) + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + + it('最新价格与用户确认金额不一致时不发送下单请求', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-price-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'visitor-token' }) + const registry = new AdapterRegistry( + settingsStore, + new PendingBookingStore(join(directory, 'pending.json')) + ) + const payload = availabilityPayload() + payload.result.slots[0]!.cost = 90 + const fetchMock = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => payload + }) + vi.stubGlobal('fetch', fetchMock) + + await expect(registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + })).rejects.toThrow('价格已变化') + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it('响应不确定时保留恢复记录并允许用户主动尝试释放', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-uncertain-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'visitor-token' }) + const registry = new AdapterRegistry( + settingsStore, + new PendingBookingStore(join(directory, 'pending.json')) + ) + const fetchMock = vi.fn() + .mockResolvedValueOnce({ ok: true, json: async () => availabilityPayload() }) + .mockRejectedValueOnce(new Error('connection reset')) + .mockResolvedValueOnce({ ok: true, json: async () => ({ successed: true }) }) + vi.stubGlobal('fetch', fetchMock) + + await expect(registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + })).rejects.toThrow('下单结果无法确认') + const uncertain = await registry.getPendingBooking(courtId) + expect(uncertain?.status).toBe('submitting-uncertain') + + await registry.cancelBooking({ courtId, orderId: uncertain!.orderId }) + await expect(registry.getPendingBooking(courtId)).resolves.toBeNull() + expect(fetchMock).toHaveBeenCalledTimes(3) + }) + + it('缺少访客凭证时不发送下单请求,也不生成恢复死锁', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-preflight-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId }) + const registry = new AdapterRegistry( + settingsStore, + new PendingBookingStore(join(directory, 'pending.json')) + ) + const fetchMock = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => availabilityPayload() + }) + vi.stubGlobal('fetch', fetchMock) + + await expect(registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + })).rejects.toThrow('PSPLVISITORID') + expect(fetchMock).toHaveBeenCalledTimes(1) + await expect(registry.getPendingBooking(courtId)).resolves.toBeNull() + }) + + it('释放响应不确定时不自动重试访客级释放接口', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-release-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'visitor-token' }) + const pendingStore = new PendingBookingStore(join(directory, 'pending.json')) + const registry = new AdapterRegistry(settingsStore, pendingStore) + await pendingStore.set({ + courtId, + orderId: 'order-1', + venueAppointmentIds: ['order-1'], + amountCents: 8000, + courtLabel: '1号风雨场', + startTime: '08:00', + endTime: '08:59', + expiresAt: '2026-07-16 08:10:00', + status: 'pending-payment' + }) + const fetchMock = vi.fn().mockRejectedValueOnce(new Error('connection reset')) + vi.stubGlobal('fetch', fetchMock) + + await expect(registry.cancelBooking({ courtId, orderId: 'order-1' })) + .rejects.toThrow('释放结果无法确认') + await expect(registry.getPendingBooking(courtId)).resolves.toMatchObject({ + status: 'release-uncertain' + }) + + await registry.cancelBooking({ courtId, orderId: 'order-1' }) + expect(fetchMock).toHaveBeenCalledTimes(1) + await expect(registry.getPendingBooking(courtId)).resolves.toBeNull() + }) + + it('下单被服务端明确拒绝时清除预写记录且不触发释放', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-rejected-create-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'visitor-token' }) + const registry = new AdapterRegistry( + settingsStore, + new PendingBookingStore(join(directory, 'pending.json')) + ) + const fetchMock = vi.fn() + .mockResolvedValueOnce({ ok: true, json: async () => availabilityPayload() }) + .mockResolvedValueOnce({ ok: false, status: 400 }) + vi.stubGlobal('fetch', fetchMock) + + await expect(registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + })).rejects.toThrow('被拒绝') + await expect(registry.getPendingBooking(courtId)).resolves.toBeNull() + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + + it('释放被服务端明确拒绝时恢复原订单状态以允许安全重试', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-rejected-release-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'visitor-token' }) + const pendingStore = new PendingBookingStore(join(directory, 'pending.json')) + const registry = new AdapterRegistry(settingsStore, pendingStore) + const pending = { + courtId, + orderId: 'order-1', + venueAppointmentIds: ['order-1'], + amountCents: 8000, + courtLabel: '1号风雨场', + startTime: '08:00', + endTime: '08:59', + expiresAt: '2026-07-16 08:10:00', + status: 'pending-payment' as const + } + await pendingStore.set(pending) + vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({ ok: false, status: 401 })) + + await expect(registry.cancelBooking({ courtId, orderId: 'order-1' })) + .rejects.toThrow('被拒绝') + await expect(registry.getPendingBooking(courtId)).resolves.toEqual(pending) + }) + + it('下单请求使用刚持久化更新的 PSPLVISITORID', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-current-credential-')) + const settingsStore = new CourtSettingsStore(join(directory, 'settings.json')) + await settingsStore.update({ courtId, visitorId: 'old-token' }) + const registry = new AdapterRegistry( + settingsStore, + new PendingBookingStore(join(directory, 'pending.json')) + ) + await registry.updateCourtSettings({ courtId, visitorId: 'updated-token' }) + await expect(registry.getCourtSettings(courtId)).resolves.toEqual({ + courtId, + visitorId: 'updated-token', + visitorIdConfigured: true + }) + const fetchMock = vi.fn() + .mockResolvedValueOnce({ ok: true, json: async () => availabilityPayload() }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + successed: true, + result: { + apptUid: 'order-current-token', + script: { expireTime: '2026-07-16 08:10:00' }, + venueAppointUids: ['order-current-token'] + } + }) + }) + vi.stubGlobal('fetch', fetchMock) + + await registry.createBooking({ + courtId, + date: '2026-07-16', + slotId, + expectedPriceCents: 8000 + }) + + const createRequest = fetchMock.mock.calls[1]?.[1] as RequestInit + expect(createRequest.headers).toEqual(expect.objectContaining({ + PSPLVISITORID: 'updated-token' + })) + }) +}) diff --git a/src/main/adapters/registry.ts b/src/main/adapters/registry.ts new file mode 100644 index 0000000..f02475a --- /dev/null +++ b/src/main/adapters/registry.ts @@ -0,0 +1,277 @@ +import type { + AvailabilityQuery, + CancelBookingInput, + CreateBookingInput, + UpdateCourtSettingsInput +} from '../../shared/contracts' +import type { CourtSettingsStore } from '../settings/CourtSettingsStore' +import type { PendingBookingStore } from '../bookings/PendingBookingStore' +import { fansiboteFuzhongAdapter } from './fansibote-fuzhong/adapter' +import { ProviderMutationError, type CourtAdapter } from './types' + +const adapters: CourtAdapter[] = [fansiboteFuzhongAdapter] + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +export class AdapterRegistry { + private readonly adapters = new Map( + adapters.map((adapter) => [adapter.court.id, adapter]) + ) + private readonly courtOperations = new Map>() + private readonly releaseConfirmedCourts = new Set() + constructor( + private readonly settingsStore: CourtSettingsStore, + private readonly pendingBookingStore: PendingBookingStore + ) {} + + listCourts() { + return [...this.adapters.values()].map((adapter) => adapter.court) + } + + async getAvailability(query: AvailabilityQuery) { + const adapter = this.getAdapter(query.courtId) + const settings = await this.settingsStore.get( + adapter.court.id, + adapter.defaultSettings + ) + return adapter.getAvailability(query, settings) + } + + async getCourtSettings(courtId: string) { + const adapter = this.getAdapter(courtId) + const settings = await this.settingsStore.get(courtId, adapter.defaultSettings) + return { + courtId, + visitorId: settings.visitorId, + visitorIdConfigured: Boolean(settings.visitorId) + } + } + + async updateCourtSettings(input: unknown) { + if ( + !isRecord(input) || + typeof input.courtId !== 'string' || + (input.visitorId !== undefined && typeof input.visitorId !== 'string') + ) { + throw new Error('球场配置参数格式错误') + } + const parsedInput: UpdateCourtSettingsInput = { + courtId: input.courtId, + visitorId: input.visitorId + } + this.getAdapter(parsedInput.courtId) + return this.runExclusive(parsedInput.courtId, async () => { + const adapter = this.getAdapter(parsedInput.courtId) + if (await this.pendingBookingStore.get(parsedInput.courtId)) { + throw new Error('当前球场存在待支付订单,请先取消释放后再修改配置') + } + const current = await this.settingsStore.get( + parsedInput.courtId, + adapter.defaultSettings + ) + const visitorId = parsedInput.visitorId?.trim() || current.visitorId + if (parsedInput.visitorId !== undefined && !parsedInput.visitorId.trim()) { + throw new Error('PSPLVISITORID 不能为空') + } + const saved = await this.settingsStore.update({ + courtId: parsedInput.courtId, + visitorId + }) + return { + courtId: saved.courtId, + visitorId: saved.visitorId, + visitorIdConfigured: Boolean(saved.visitorId) + } + }) + } + + async createBooking(input: unknown) { + const parsedInput = this.parseCreateBookingInput(input) + return this.runExclusive(parsedInput.courtId, () => + this.createBookingExclusive(parsedInput) + ) + } + + async cancelBooking(input: unknown) { + const parsedInput = this.parseCancelBookingInput(input) + return this.runExclusive(parsedInput.courtId, () => + this.cancelBookingExclusive(parsedInput) + ) + } + + async getPendingBooking(courtId: string) { + this.getAdapter(courtId) + const pending = await this.pendingBookingStore.get(courtId) + if (pending?.status === 'release-confirmed') { + await this.pendingBookingStore.clear(courtId) + return null + } + return pending + } + + private async createBookingExclusive(parsedInput: CreateBookingInput) { + if (await this.pendingBookingStore.get(parsedInput.courtId)) { + throw new Error('当前球场已有待支付订单,请先取消释放') + } + + const adapter = this.getAdapter(parsedInput.courtId) + if (!adapter.createBooking) throw new Error('当前球场尚未接入下单流程') + const settings = await this.settingsStore.get( + adapter.court.id, + adapter.defaultSettings + ) + const availability = await adapter.getAvailability({ + courtId: parsedInput.courtId, + date: parsedInput.date + }, settings) + const slot = availability.slots.find((candidate) => candidate.id === parsedInput.slotId) + if (!slot) throw new Error('目标时段已不存在,请刷新后重试') + if (slot.status !== 'available' && slot.status !== 'limited') { + throw new Error('目标时段已不可预约,请刷新列表') + } + if (slot.enrollment) throw new Error('活动时段不能通过普通场地接口下单') + if (slot.priceCents !== parsedInput.expectedPriceCents) { + throw new Error('价格已变化,请刷新列表并重新确认金额') + } + adapter.preflightWriteSettings?.(settings) + + const uncertainBooking = { + courtId: parsedInput.courtId, + orderId: `uncertain:${Date.now()}`, + venueAppointmentIds: [], + amountCents: slot.priceCents, + courtLabel: slot.courtLabel, + startTime: slot.startTime, + endTime: slot.endTime, + status: 'submitting-uncertain' as const + } + await this.pendingBookingStore.set(uncertainBooking) + + let result + try { + result = await adapter.createBooking(slot, settings) + } catch (error) { + if (error instanceof ProviderMutationError && error.outcome === 'rejected') { + await this.pendingBookingStore.clear(parsedInput.courtId) + throw error + } + throw new Error( + `下单结果无法确认,已保留恢复记录;请刷新列表后使用“尝试释放”。${error instanceof Error ? ` 原因:${error.message}` : ''}` + ) + } + try { + await this.pendingBookingStore.set(result.booking) + } catch { + try { + await this.pendingBookingStore.set({ + ...uncertainBooking, + status: 'release-uncertain' + }) + await adapter.cancelBooking?.(settings) + this.releaseConfirmedCourts.add(parsedInput.courtId) + await this.pendingBookingStore.set({ + ...uncertainBooking, + status: 'release-confirmed' + }) + await this.pendingBookingStore.clear(parsedInput.courtId) + this.releaseConfirmedCourts.delete(parsedInput.courtId) + } catch { + throw new Error('锁场成功但本机记录和自动释放均失败,请立即到小程序检查订单') + } + throw new Error('锁场后的本机记录失败,场地已自动释放') + } + return result.booking + } + + private async cancelBookingExclusive(input: CancelBookingInput) { + const pending = await this.pendingBookingStore.get(input.courtId) + if (!pending || pending.orderId !== input.orderId) { + throw new Error('没有找到对应的待支付锁场订单') + } + if ( + pending.status === 'release-confirmed' || + this.releaseConfirmedCourts.has(input.courtId) || + pending.status === 'release-uncertain' + ) { + await this.pendingBookingStore.clear(input.courtId) + this.releaseConfirmedCourts.delete(input.courtId) + return + } + const adapter = this.getAdapter(input.courtId) + if (!adapter.cancelBooking) throw new Error('当前球场尚未接入取消流程') + const settings = await this.settingsStore.get( + adapter.court.id, + adapter.defaultSettings + ) + adapter.preflightWriteSettings?.(settings) + await this.pendingBookingStore.set({ ...pending, status: 'release-uncertain' }) + try { + await adapter.cancelBooking(settings) + } catch (error) { + if (error instanceof ProviderMutationError && error.outcome === 'rejected') { + await this.pendingBookingStore.set(pending) + throw error + } + throw new Error( + `释放结果无法确认;为避免重复释放新订单,已停止自动重试。请先到小程序核实,再清理本机记录。${error instanceof Error ? ` 原因:${error.message}` : ''}` + ) + } + this.releaseConfirmedCourts.add(input.courtId) + await this.pendingBookingStore.set({ ...pending, status: 'release-confirmed' }) + await this.pendingBookingStore.clear(input.courtId) + this.releaseConfirmedCourts.delete(input.courtId) + } + + private parseCreateBookingInput(input: unknown): CreateBookingInput { + if ( + !isRecord(input) || + typeof input.courtId !== 'string' || + typeof input.date !== 'string' || + typeof input.slotId !== 'string' || + typeof input.expectedPriceCents !== 'number' || + !Number.isSafeInteger(input.expectedPriceCents) || + input.expectedPriceCents < 0 + ) { + throw new Error('下单参数格式错误') + } + return { + courtId: input.courtId, + date: input.date, + slotId: input.slotId, + expectedPriceCents: input.expectedPriceCents + } + } + + private parseCancelBookingInput(input: unknown): CancelBookingInput { + if ( + !isRecord(input) || + typeof input.courtId !== 'string' || + typeof input.orderId !== 'string' + ) { + throw new Error('取消锁场参数格式错误') + } + return { courtId: input.courtId, orderId: input.orderId } + } + + private async runExclusive(courtId: string, operation: () => Promise): Promise { + const previous = this.courtOperations.get(courtId) ?? Promise.resolve() + const result = previous.catch(() => undefined).then(operation) + const tail = result.then(() => undefined, () => undefined) + this.courtOperations.set(courtId, tail) + try { + return await result + } finally { + if (this.courtOperations.get(courtId) === tail) { + this.courtOperations.delete(courtId) + } + } + } + + private getAdapter(courtId: string) { + const adapter = this.adapters.get(courtId) + if (!adapter) throw new Error(`Unknown court adapter: ${courtId}`) + return adapter + } +} diff --git a/src/main/adapters/types.ts b/src/main/adapters/types.ts new file mode 100644 index 0000000..8981e8f --- /dev/null +++ b/src/main/adapters/types.ts @@ -0,0 +1,38 @@ +import type { + AvailabilityDay, + AvailabilityQuery, + BookingSlot, + PendingBooking, + CourtSummary +} from '../../shared/contracts' +import type { CourtRuntimeSettings } from '../settings/types' + +export interface AdapterBookingResult { + booking: PendingBooking + paymentScript: unknown +} + +export class ProviderMutationError extends Error { + constructor( + message: string, + readonly outcome: 'rejected' | 'uncertain' + ) { + super(message) + this.name = 'ProviderMutationError' + } +} + +export interface CourtAdapter { + readonly court: CourtSummary + readonly defaultSettings: CourtRuntimeSettings + getAvailability( + query: AvailabilityQuery, + settings: CourtRuntimeSettings + ): Promise + preflightWriteSettings?(settings: CourtRuntimeSettings): void + createBooking?( + slot: BookingSlot, + settings: CourtRuntimeSettings + ): Promise + cancelBooking?(settings: CourtRuntimeSettings): Promise +} diff --git a/src/main/bookings/PendingBookingStore.test.ts b/src/main/bookings/PendingBookingStore.test.ts new file mode 100644 index 0000000..33a6b95 --- /dev/null +++ b/src/main/bookings/PendingBookingStore.test.ts @@ -0,0 +1,32 @@ +import { mkdtemp } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { describe, expect, it } from 'vitest' +import type { PendingBooking } from '../../shared/contracts' +import { PendingBookingStore } from './PendingBookingStore' + +const booking: PendingBooking = { + courtId: 'court-a', + orderId: 'order-1', + venueAppointmentIds: ['order-1'], + amountCents: 8000, + courtLabel: '1号场', + startTime: '08:00', + endTime: '08:59', + expiresAt: '2026-07-16 08:10:00', + status: 'pending-payment' +} + +describe('PendingBookingStore', () => { + it('可在重启式重新实例化后恢复和清除待支付订单', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-pending-')) + const filePath = join(directory, 'pending.json') + const store = new PendingBookingStore(filePath) + await store.set(booking) + + const reloaded = new PendingBookingStore(filePath) + await expect(reloaded.get('court-a')).resolves.toEqual(booking) + await reloaded.clear('court-a') + await expect(reloaded.get('court-a')).resolves.toBeNull() + }) +}) diff --git a/src/main/bookings/PendingBookingStore.ts b/src/main/bookings/PendingBookingStore.ts new file mode 100644 index 0000000..060bf8b --- /dev/null +++ b/src/main/bookings/PendingBookingStore.ts @@ -0,0 +1,99 @@ +import { dirname } from 'node:path' +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises' +import type { PendingBooking } from '../../shared/contracts' + +interface BookingFile { + version: 1 + courts: Record +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function parseBooking(value: unknown): PendingBooking { + if ( + !isRecord(value) || + typeof value.courtId !== 'string' || + typeof value.orderId !== 'string' || + !Array.isArray(value.venueAppointmentIds) || + !value.venueAppointmentIds.every((id) => typeof id === 'string') || + typeof value.amountCents !== 'number' || + typeof value.courtLabel !== 'string' || + typeof value.startTime !== 'string' || + typeof value.endTime !== 'string' || + (value.expiresAt !== undefined && typeof value.expiresAt !== 'string') || + ![ + 'submitting-uncertain', + 'pending-payment', + 'release-uncertain', + 'release-confirmed' + ].includes( + String(value.status) + ) + ) { + throw new Error('本机待支付订单记录格式错误') + } + return value as unknown as PendingBooking +} + +export class PendingBookingStore { + private writeQueue: Promise = Promise.resolve() + + constructor(private readonly filePath: string) {} + + async get(courtId: string): Promise { + return (await this.read()).courts[courtId] ?? null + } + + set(booking: PendingBooking): Promise { + return this.write((file) => { + file.courts[booking.courtId] = booking + }) + } + + clear(courtId: string): Promise { + return this.write((file) => { + delete file.courts[courtId] + }) + } + + private async write(update: (file: BookingFile) => void): Promise { + const operation = this.writeQueue.then(async () => { + const file = await this.read() + update(file) + await mkdir(dirname(this.filePath), { recursive: true }) + const temporaryPath = `${this.filePath}.${process.pid}.tmp` + await writeFile(temporaryPath, `${JSON.stringify(file, null, 2)}\n`, { + encoding: 'utf8', + mode: 0o600 + }) + await rename(temporaryPath, this.filePath) + }) + this.writeQueue = operation.catch(() => undefined) + return operation + } + + private async read(): Promise { + try { + const value: unknown = JSON.parse(await readFile(this.filePath, 'utf8')) + if (!isRecord(value) || value.version !== 1 || !isRecord(value.courts)) { + throw new Error('本机待支付订单文件格式错误') + } + return { + version: 1, + courts: Object.fromEntries( + Object.entries(value.courts).map(([courtId, booking]) => [ + courtId, + parseBooking(booking) + ]) + ) + } + } catch (error) { + if (isRecord(error) && error.code === 'ENOENT') { + return { version: 1, courts: {} } + } + throw error + } + } +} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..411572c --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,91 @@ +import { join } from 'node:path' +import { app, BrowserWindow, ipcMain, shell } from 'electron' +import { AdapterRegistry } from './adapters/registry' +import { CourtSettingsStore } from './settings/CourtSettingsStore' +import { PendingBookingStore } from './bookings/PendingBookingStore' +import { + IPC_CHANNELS, + type AvailabilityQuery +} from '../shared/contracts' + +function createWindow() { + const mainWindow = new BrowserWindow({ + width: 1440, + height: 920, + minWidth: 1040, + minHeight: 680, + show: false, + backgroundColor: '#f4f1e8', + titleBarStyle: 'hiddenInset', + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true + } + }) + + mainWindow.once('ready-to-show', () => mainWindow.show()) + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https://')) void shell.openExternal(url) + return { action: 'deny' } + }) + mainWindow.webContents.on('will-navigate', (event) => event.preventDefault()) + + if (process.env.ELECTRON_RENDERER_URL) { + void mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) + } else { + void mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } +} + +app.whenReady().then(() => { + const settingsStore = new CourtSettingsStore( + join(app.getPath('userData'), 'court-settings.json') + ) + const pendingBookingStore = new PendingBookingStore( + join(app.getPath('userData'), 'pending-bookings.json') + ) + const registry = new AdapterRegistry(settingsStore, pendingBookingStore) + + ipcMain.handle(IPC_CHANNELS.listCourts, () => registry.listCourts()) + ipcMain.handle( + IPC_CHANNELS.getAvailability, + (_event, query: AvailabilityQuery) => registry.getAvailability(query) + ) + ipcMain.handle( + IPC_CHANNELS.getCourtSettings, + (_event, courtId: unknown) => { + if (typeof courtId !== 'string') throw new Error('球场标识格式错误') + return registry.getCourtSettings(courtId) + } + ) + ipcMain.handle( + IPC_CHANNELS.updateCourtSettings, + (_event, input: unknown) => registry.updateCourtSettings(input) + ) + ipcMain.handle( + IPC_CHANNELS.createBooking, + (_event, input: unknown) => registry.createBooking(input) + ) + ipcMain.handle( + IPC_CHANNELS.cancelBooking, + (_event, input: unknown) => registry.cancelBooking(input) + ) + ipcMain.handle( + IPC_CHANNELS.getPendingBooking, + (_event, courtId: unknown) => { + if (typeof courtId !== 'string') throw new Error('球场标识格式错误') + return registry.getPendingBooking(courtId) + } + ) + + createWindow() + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/src/main/settings/CourtSettingsStore.test.ts b/src/main/settings/CourtSettingsStore.test.ts new file mode 100644 index 0000000..201a3b0 --- /dev/null +++ b/src/main/settings/CourtSettingsStore.test.ts @@ -0,0 +1,67 @@ +import { mkdtemp, readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { describe, expect, it } from 'vitest' +import { CourtSettingsStore } from './CourtSettingsStore' + +describe('CourtSettingsStore', () => { + it('未配置时返回对应球场的默认值', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-settings-')) + const store = new CourtSettingsStore(join(directory, 'court-settings.json')) + + await expect(store.get('court-a', { + courtId: 'court-a' + })).resolves.toEqual({ courtId: 'court-a' }) + }) + + it('按球场隔离并持久化访客凭证', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-settings-')) + const filePath = join(directory, 'court-settings.json') + const store = new CourtSettingsStore(filePath) + + await store.update({ + courtId: 'court-a', + visitorId: 'court-a-secret' + }) + await store.update({ courtId: 'court-b' }) + + const reloaded = new CourtSettingsStore(filePath) + await expect(reloaded.get('court-a', { + courtId: 'court-a' + })).resolves.toEqual({ + courtId: 'court-a', + visitorId: 'court-a-secret' + }) + await expect(reloaded.get('court-b', { + courtId: 'court-b' + })).resolves.toEqual({ courtId: 'court-b' }) + + const persisted = JSON.parse(await readFile(filePath, 'utf8')) as unknown + expect(persisted).toEqual({ + version: 1, + courts: { + 'court-a': { visitorId: 'court-a-secret' }, + 'court-b': {} + } + }) + }) + + it('忽略旧版暴露的场地 ID,只迁移访客凭证', async () => { + const directory = await mkdtemp(join(tmpdir(), 'tennis-book-settings-legacy-')) + const filePath = join(directory, 'court-settings.json') + await writeFile(filePath, JSON.stringify({ + version: 1, + courts: { + 'court-a': { userId: '6038652', visitorId: 'legacy-secret' } + } + })) + + const store = new CourtSettingsStore(filePath) + await expect(store.get('court-a', { + courtId: 'court-a' + })).resolves.toEqual({ + courtId: 'court-a', + visitorId: 'legacy-secret' + }) + }) +}) diff --git a/src/main/settings/CourtSettingsStore.ts b/src/main/settings/CourtSettingsStore.ts new file mode 100644 index 0000000..6ada4e3 --- /dev/null +++ b/src/main/settings/CourtSettingsStore.ts @@ -0,0 +1,81 @@ +import { dirname } from 'node:path' +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises' +import type { CourtRuntimeSettings } from './types' + +interface SettingsFile { + version: 1 + courts: Record +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function parseSettingsFile(value: unknown): SettingsFile { + if (!isRecord(value) || value.version !== 1 || !isRecord(value.courts)) { + throw new Error('本机球场配置文件格式错误') + } + + const courts: SettingsFile['courts'] = {} + for (const [courtId, settings] of Object.entries(value.courts)) { + if ( + !isRecord(settings) || + (settings.visitorId !== undefined && typeof settings.visitorId !== 'string') + ) { + throw new Error(`球场 ${courtId} 的本机配置格式错误`) + } + courts[courtId] = { + visitorId: settings.visitorId + } + } + + return { version: 1, courts } +} + +export class CourtSettingsStore { + private writeQueue: Promise = Promise.resolve() + + constructor(private readonly filePath: string) {} + + async get( + courtId: string, + defaults: CourtRuntimeSettings + ): Promise { + const file = await this.read() + return { + courtId, + visitorId: file.courts[courtId]?.visitorId ?? defaults.visitorId + } + } + + async update(settings: CourtRuntimeSettings): Promise { + const operation = this.writeQueue.then(async () => { + const file = await this.read() + file.courts[settings.courtId] = { + visitorId: settings.visitorId + } + await mkdir(dirname(this.filePath), { recursive: true }) + const temporaryPath = `${this.filePath}.${process.pid}.tmp` + await writeFile(temporaryPath, `${JSON.stringify(file, null, 2)}\n`, { + encoding: 'utf8', + mode: 0o600 + }) + await rename(temporaryPath, this.filePath) + }) + + this.writeQueue = operation.catch(() => undefined) + await operation + return settings + } + + private async read(): Promise { + try { + return parseSettingsFile(JSON.parse(await readFile(this.filePath, 'utf8'))) + } catch (error) { + if (isRecord(error) && error.code === 'ENOENT') { + return { version: 1, courts: {} } + } + throw error + } + } +} diff --git a/src/main/settings/types.ts b/src/main/settings/types.ts new file mode 100644 index 0000000..a67f2fe --- /dev/null +++ b/src/main/settings/types.ts @@ -0,0 +1,4 @@ +export interface CourtRuntimeSettings { + courtId: string + visitorId?: string +} diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..5b994d3 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,27 @@ +import { contextBridge, ipcRenderer } from 'electron' +import { + IPC_CHANNELS, + type AvailabilityQuery, + type CancelBookingInput, + type CreateBookingInput, + type TennisBookApi, + type UpdateCourtSettingsInput +} from '../shared/contracts' + +const api: TennisBookApi = { + listCourts: () => ipcRenderer.invoke(IPC_CHANNELS.listCourts), + getAvailability: (query: AvailabilityQuery) => + ipcRenderer.invoke(IPC_CHANNELS.getAvailability, query), + getCourtSettings: (courtId: string) => + ipcRenderer.invoke(IPC_CHANNELS.getCourtSettings, courtId), + updateCourtSettings: (input: UpdateCourtSettingsInput) => + ipcRenderer.invoke(IPC_CHANNELS.updateCourtSettings, input), + createBooking: (input: CreateBookingInput) => + ipcRenderer.invoke(IPC_CHANNELS.createBooking, input), + cancelBooking: (input: CancelBookingInput) => + ipcRenderer.invoke(IPC_CHANNELS.cancelBooking, input), + getPendingBooking: (courtId: string) => + ipcRenderer.invoke(IPC_CHANNELS.getPendingBooking, courtId) +} + +contextBridge.exposeInMainWorld('tennisBook', api) diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..0bb169a --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + + + Tennis Book + + +
+ + + diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx new file mode 100644 index 0000000..29a7dfb --- /dev/null +++ b/src/renderer/src/App.tsx @@ -0,0 +1,792 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + CalendarDays, + ChevronDown, + CircleAlert, + CircleCheck, + Clock3, + Command, + Eye, + EyeOff, + MapPin, + LockKeyhole, + RefreshCw, + Search, + Settings2, + SlidersHorizontal, + Sparkles, + SunMedium, + UnlockKeyhole, + Warehouse, + Wifi +} from 'lucide-react' +import type { + AvailabilityDay, + BookingSlot, + CourtSettings, + CourtStatus, + CourtSummary, + PendingBooking +} from '../../shared/contracts' + +const dateFormatter = new Intl.DateTimeFormat('zh-CN', { + month: 'short', + day: 'numeric' +}) +const weekdayFormatter = new Intl.DateTimeFormat('zh-CN', { weekday: 'short' }) +const priceFormatter = new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + maximumFractionDigits: 0 +}) + +interface DateOption { + value: string + date: Date + relativeLabel?: string +} + +function toLocalDateValue(date: Date) { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +function createDateOptions(): DateOption[] { + return Array.from({ length: 7 }, (_, index) => { + const date = new Date() + date.setHours(12, 0, 0, 0) + date.setDate(date.getDate() + index) + return { + value: toLocalDateValue(date), + date, + relativeLabel: index === 0 ? '今天' : index === 1 ? '明天' : undefined + } + }) +} + +const dateOptions = createDateOptions() +const tennisBookApi = window.tennisBook + +const statusCopy: Record = { + online: '可查询', + maintenance: '维护中', + 'login-required': '需登录' +} + +function getErrorMessage(error: unknown, fallback: string) { + return error instanceof Error && error.message ? error.message : fallback +} + +function CourtIcon({ court }: { court: CourtSummary }) { + return ( +
+ {court.shortName.slice(0, 1)} + + ) +} + +function CourtRow({ + court, + selected, + onSelect +}: { + court: CourtSummary + selected: boolean + onSelect: () => void +}) { + const metadata = [ + court.district, + court.distanceKm === undefined ? undefined : `${court.distanceKm} km` + ].filter(Boolean).join(' · ') + + return ( + + ) +} + +function SlotCell({ + slot, + selected, + onSelect +}: { + slot?: BookingSlot + selected: boolean + onSelect: () => void +}) { + if (!slot) return
+ + const isAvailable = slot.status === 'available' || slot.status === 'limited' + const bookingContacts = slot.status === 'booked' ? slot.bookedBy ?? [] : [] + const enrollment = slot.enrollment + return ( + + ) +} + +export function App() { + const [courts, setCourts] = useState([]) + const [selectedCourtId, setSelectedCourtId] = useState('') + const [selectedDate, setSelectedDate] = useState(dateOptions[0]?.value ?? '') + const [availability, setAvailability] = useState(null) + const [search, setSearch] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedSlotId, setSelectedSlotId] = useState(null) + const [refreshToken, setRefreshToken] = useState(0) + const [settingsOpen, setSettingsOpen] = useState(false) + const [courtSettings, setCourtSettings] = useState(null) + const [visitorIdInput, setVisitorIdInput] = useState('') + const [visitorIdVisible, setVisitorIdVisible] = useState(false) + const [settingsLoading, setSettingsLoading] = useState(false) + const [settingsSaving, setSettingsSaving] = useState(false) + const [settingsError, setSettingsError] = useState(null) + const [pendingBooking, setPendingBooking] = useState(null) + const [bookingConfirmOpen, setBookingConfirmOpen] = useState(false) + const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false) + const [bookingBusy, setBookingBusy] = useState(false) + const [bookingError, setBookingError] = useState(null) + const settingsRequestId = useRef(0) + + useEffect(() => { + if (!tennisBookApi) { + setLoading(false) + return + } + let active = true + tennisBookApi + .listCourts() + .then((items) => { + if (!active) return + setCourts(items) + setSelectedCourtId((current) => current || items[0]?.id || '') + }) + .catch(() => { + if (!active) return + setError('球场列表加载失败,请稍后重试。') + setLoading(false) + }) + return () => { + active = false + } + }, []) + + useEffect(() => { + if (!tennisBookApi || !selectedCourtId || !selectedDate) return + let active = true + setLoading(true) + setError(null) + setAvailability(null) + setSelectedSlotId(null) + + tennisBookApi + .getAvailability({ courtId: selectedCourtId, date: selectedDate }) + .then((result) => active && setAvailability(result)) + .catch(() => { + if (!active) return + setAvailability(null) + setError('可订时段加载失败,请检查登录状态或网络。') + }) + .finally(() => active && setLoading(false)) + + return () => { + active = false + } + }, [selectedCourtId, selectedDate, refreshToken]) + + useEffect(() => { + if (!tennisBookApi || !selectedCourtId) return + let active = true + setPendingBooking(null) + tennisBookApi.getPendingBooking(selectedCourtId) + .then((booking) => active && setPendingBooking(booking)) + .catch(() => active && setBookingError('待支付订单状态读取失败。')) + return () => { + active = false + } + }, [selectedCourtId]) + + const selectedCourt = courts.find((court) => court.id === selectedCourtId) + const filteredCourts = useMemo(() => { + const keyword = search.trim().toLowerCase() + if (!keyword) return courts + return courts.filter((court) => + `${court.name}${court.district ?? ''}${court.address ?? ''}`.toLowerCase().includes(keyword) + ) + }, [courts, search]) + + const schedule = useMemo(() => { + const slots = availability?.slots ?? [] + const courtLabels = [...new Set(slots.map((slot) => slot.courtLabel))] + const times = [...new Set(slots.map((slot) => slot.startTime))] + const slotMap = new Map(slots.map((slot) => [`${slot.startTime}:${slot.courtLabel}`, slot])) + return { courtLabels, times, slotMap } + }, [availability]) + + const availableCount = availability?.slots.filter( + (slot) => slot.status === 'available' || slot.status === 'limited' + ).length ?? 0 + const selectedSlot = availability?.slots.find((slot) => slot.id === selectedSlotId) + const refresh = useCallback(() => setRefreshToken((value) => value + 1), []) + const closeSettings = useCallback(() => { + settingsRequestId.current += 1 + setSettingsOpen(false) + setCourtSettings(null) + setVisitorIdInput('') + setVisitorIdVisible(false) + }, []) + const openSettings = useCallback(async () => { + if (!tennisBookApi || !selectedCourtId) return + const requestId = settingsRequestId.current + 1 + settingsRequestId.current = requestId + setSettingsOpen(true) + setSettingsLoading(true) + setSettingsError(null) + setCourtSettings(null) + setVisitorIdInput('') + setVisitorIdVisible(false) + try { + const settings = await tennisBookApi.getCourtSettings(selectedCourtId) + if (settingsRequestId.current !== requestId) return + setCourtSettings(settings) + setVisitorIdInput(settings.visitorId ?? '') + } catch { + if (settingsRequestId.current !== requestId) return + setSettingsError('球场配置读取失败,请稍后重试。') + } finally { + if (settingsRequestId.current === requestId) setSettingsLoading(false) + } + }, [selectedCourtId]) + + const saveSettings = useCallback(async (event: React.FormEvent) => { + event.preventDefault() + if (!tennisBookApi || !courtSettings) return + setSettingsSaving(true) + setSettingsError(null) + try { + const visitorId = visitorIdInput.trim() + const saved = await tennisBookApi.updateCourtSettings({ + courtId: courtSettings.courtId, + ...(visitorId ? { visitorId } : {}) + }) + setCourtSettings(saved) + setVisitorIdInput('') + closeSettings() + setRefreshToken((value) => value + 1) + } catch (error) { + setSettingsError(getErrorMessage( + error, + '配置保存失败,请检查访客凭证。' + )) + } finally { + setSettingsSaving(false) + } + }, [closeSettings, courtSettings, visitorIdInput]) + + const createBooking = useCallback(async () => { + if (!tennisBookApi || !selectedSlot || !selectedCourtId) return + setBookingBusy(true) + setBookingError(null) + try { + const booking = await tennisBookApi.createBooking({ + courtId: selectedCourtId, + date: selectedDate, + slotId: selectedSlot.id, + expectedPriceCents: selectedSlot.priceCents + }) + setPendingBooking(booking) + setBookingConfirmOpen(false) + setSelectedSlotId(null) + setRefreshToken((value) => value + 1) + } catch (error) { + setBookingError(getErrorMessage(error, '下单锁场失败,请刷新后重试。')) + const recoveryBooking = await tennisBookApi.getPendingBooking(selectedCourtId) + if (recoveryBooking?.status === 'submitting-uncertain') { + setPendingBooking(recoveryBooking) + setBookingConfirmOpen(false) + setSelectedSlotId(null) + } + } finally { + setBookingBusy(false) + } + }, [selectedCourtId, selectedDate, selectedSlot]) + + const cancelBooking = useCallback(async () => { + if (!tennisBookApi || !pendingBooking) return + setBookingBusy(true) + setBookingError(null) + try { + await tennisBookApi.cancelBooking({ + courtId: pendingBooking.courtId, + orderId: pendingBooking.orderId + }) + setPendingBooking(null) + setCancelConfirmOpen(false) + setRefreshToken((value) => value + 1) + } catch (error) { + setBookingError(getErrorMessage(error, '取消锁场失败,请稍后重试。')) + const latestPending = await tennisBookApi.getPendingBooking(pendingBooking.courtId) + setPendingBooking(latestPending) + } finally { + setBookingBusy(false) + } + }, [pendingBooking]) + + if (!tennisBookApi) { + return ( +
+ PRELOAD ERROR +

桌面通信模块未加载

+

preload 构建产物与主进程配置不一致。

+ 请重启开发服务;如果仍然出现,把这段错误文字发给开发者。 +
+ ) + } + + return ( +
+
+
+ + + +
+ {selectedCourt ? ( + <> +
+
+ +
+
+

{selectedCourt.name}

+ + {selectedCourt.status === 'online' ? : } + {statusCopy[selectedCourt.status]} + +
+

{selectedCourt.address ?? '地址待补充'}

+
+
+
+
场地类型{selectedCourt.surface ?? '待补充'}
+
可订时段{loading ? '—' : `${availableCount} 个`}
+ +
+
+ +
+
+ {dateOptions.map((option) => ( + + ))} + +
+ +
+
+ LIVE AVAILABILITY +

可预约时段

+
+
+ 可订 + 双打活动 + 已订 + 排课 +
+
+ + {error ?
{error}
: null} + {bookingError ? ( +
+ + {bookingError} + +
+ ) : null} + {loading ? ( +
+ {Array.from({ length: 28 }, (_, index) => )} +
+ ) : ( +
+
+
时间
+ {schedule.courtLabels.map((label) =>
{label}
)} + {schedule.times.map((time) => ( +
+
{time}60 分钟
+ {schedule.courtLabels.map((label) => { + const slot = schedule.slotMap.get(`${time}:${label}`) + return ( + slot && setSelectedSlotId(slot.id)} + selected={slot?.id === selectedSlotId} + slot={slot} + /> + ) + })} +
+ ))} +
+
+ )} +
+ + {pendingBooking ? ( +
+ +
+ {pendingBooking.status === 'submitting-uncertain' + ? '下单结果待确认' + : pendingBooking.status === 'release-uncertain' + ? '释放结果待确认' + : '已锁场 · 待支付'} + + {pendingBooking.startTime}–{pendingBooking.endTime} · {pendingBooking.courtLabel} + + {pendingBooking.status === 'submitting-uncertain' + ? '网络响应不确定,请刷新列表后尝试释放该访客凭证下的锁场' + : pendingBooking.status === 'release-uncertain' + ? '请先到小程序核实是否已释放,再清理本机记录;系统不会自动重试' + : `订单 ${pendingBooking.orderId} · 支付有效期至 ${pendingBooking.expiresAt}`} +
+
+ {priceFormatter.format(pendingBooking.amountCents / 100)} +
+ +
+ ) : ( +
+
+ 当前选择 + {selectedSlot + ? selectedSlot.enrollment + ? `${selectedSlot.enrollment.title} · ${selectedSlot.courtLabel}` + : `${selectedSlot.startTime}–${selectedSlot.endTime} · ${selectedSlot.courtLabel}` + : '选择一个可订时段'} +
+
+ {selectedSlot + ? priceFormatter.format( + (selectedSlot.enrollment?.feeCents ?? selectedSlot.priceCents) / 100 + ) + : '—'} +
+ +
+ )} + + ) : ( + error ? ( +
+ + 球场加载失败 + {error} +
+ ) : ( +
请选择一个球场
+ ) + )} +
+ + {settingsOpen ? ( +
{ + if (event.currentTarget === event.target && !settingsSaving) { + closeSettings() + } + }} + role="presentation" + > +
void saveSettings(event)}> +
+ +
+ COURT CONNECTION +

球场接口配置

+
+
+ +
+ 当前球场 + {selectedCourt?.name ?? '未选择球场'} + 配置按球场独立保存,不会影响其他球场。 +
+ + {settingsLoading ? ( +
正在读取本机配置…
+ ) : ( + + )} + + {settingsError ? ( +
{settingsError}
+ ) : null} + +
+ + +
+
+
+ ) : null} + + {bookingConfirmOpen && selectedSlot ? ( +
+
+ + CREATE VENUE ORDER +

确认下单并锁定场地?

+

此操作只创建待支付订单,不会自动支付。下单成功后场地会被临时锁定。

+
+
场地{selectedCourt?.name} · {selectedSlot.courtLabel}
+
时间{selectedDate} {selectedSlot.startTime}–{selectedSlot.endTime}
+
金额{priceFormatter.format(selectedSlot.priceCents / 100)}
+
+ {bookingError ? ( +
{bookingError}
+ ) : null} +
+ + +
+
+
+ ) : null} + + {cancelConfirmOpen && pendingBooking ? ( +
+
+ + RELEASE VENUE HOLD +

{pendingBooking.status === 'release-uncertain' + ? '确认已经在小程序核实?' + : '取消订单并释放场地?'}

+

{pendingBooking.status === 'release-uncertain' + ? '这里只清理本机的待确认记录,不会再次调用释放接口。请确认场地已经释放或你已自行处理。' + : '将调用该球场的释放接口。成功后当前待支付订单不能继续用于支付。'}

+
+
订单{pendingBooking.status === 'submitting-uncertain' + ? '结果待确认' + : pendingBooking.orderId}
+
场地{pendingBooking.courtLabel}
+
时间{pendingBooking.startTime}–{pendingBooking.endTime}
+
+ {bookingError ? ( +
{bookingError}
+ ) : null} +
+ + +
+
+
+ ) : null} +
+ ) +} diff --git a/src/renderer/src/components/AppErrorBoundary.tsx b/src/renderer/src/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..fb65198 --- /dev/null +++ b/src/renderer/src/components/AppErrorBoundary.tsx @@ -0,0 +1,34 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react' + +interface Props { + children: ReactNode +} + +interface State { + error: Error | null +} + +export class AppErrorBoundary extends Component { + state: State = { error: null } + + static getDerivedStateFromError(error: Error): State { + return { error } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error('Renderer failed to start', error, info.componentStack) + } + + render() { + if (!this.state.error) return this.props.children + + return ( +
+ APPLICATION ERROR +

界面启动失败

+

{this.state.error.message}

+ 请重启应用;如果仍然出现,把这段错误文字发给开发者。 +
+ ) + } +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx new file mode 100644 index 0000000..34871bb --- /dev/null +++ b/src/renderer/src/main.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import '@fontsource-variable/manrope' +import '@fontsource-variable/newsreader' +import { App } from './App' +import { AppErrorBoundary } from './components/AppErrorBoundary' +import './styles.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + +) diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css new file mode 100644 index 0000000..76e53fa --- /dev/null +++ b/src/renderer/src/styles.css @@ -0,0 +1,399 @@ +:root { + color-scheme: light; + font-family: 'Manrope Variable', sans-serif; + color: #173328; + background: #f4f1e8; + font-synthesis: none; + text-rendering: optimizeLegibility; + --ink: #173328; + --ink-soft: #496157; + --muted: #7d8b83; + --canvas: #f4f1e8; + --paper: #fffef9; + --sidebar: #edf1e7; + --line: #dfe4da; + --line-strong: #cbd4c7; + --green: #174f3a; + --green-bright: #257452; + --tennis: #d7f45c; + --tennis-soft: #eff9c9; + --coral: #b95b48; + --coral-soft: #fff0eb; + --sand: #9b7541; + --sand-soft: #f8f0df; + --shadow: 0 12px 32px rgba(28, 61, 47, .08); +} + +* { box-sizing: border-box; } +html, body, #root { min-width: 100%; min-height: 100%; margin: 0; } +body { overflow: hidden; } +button, input { font: inherit; } +button { color: inherit; } + +.app-shell { + display: grid; + grid-template: 52px minmax(0, 1fr) / 278px minmax(0, 1fr); + height: 100vh; + background: var(--canvas); +} + +.titlebar { + -webkit-app-region: drag; + z-index: 20; + grid-column: 1 / -1; + display: grid; + grid-template-columns: 72px 29px 220px 1fr 40px; + align-items: center; + height: 52px; + padding: 0 12px 0 0; + border-bottom: 1px solid var(--line); + background: rgba(255, 254, 249, .94); + backdrop-filter: blur(18px); +} +.traffic-light-space { width: 72px; } +.brand-mark { + display: grid; + place-items: center; + width: 29px; + height: 29px; + border-radius: 9px; + color: var(--tennis); + background: var(--green); + box-shadow: 0 4px 10px rgba(23, 79, 58, .18); +} +.brand-mark span { font-size: 9px; font-weight: 850; letter-spacing: -.04em; } +.titlebar-name { margin-left: 10px; color: var(--ink); font-size: 12px; font-weight: 750; } +.titlebar-center { justify-self: center; } +.connection-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + border: 1px solid #d8e6d9; + border-radius: 999px; + color: #5e7567; + background: #f5faf1; + font-size: 9px; + font-weight: 650; +} +.connection-pill svg { color: #32905f; } +.titlebar-action, .icon-button, .refresh-button, .calendar-button { -webkit-app-region: no-drag; } + +.icon-button { + display: grid; + place-items: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid transparent; + border-radius: 9px; + color: #6f8076; + background: transparent; + cursor: pointer; + transition: .15s ease; +} +.icon-button:hover { border-color: var(--line); color: var(--green); background: #f8faf5; } +.icon-button:disabled { cursor: not-allowed; opacity: .38; } + +.sidebar { + display: flex; + min-height: 0; + flex-direction: column; + border-right: 1px solid #d9dfd4; + background: + linear-gradient(rgba(255,255,255,.16), rgba(255,255,255,0)), + var(--sidebar); +} +.sidebar-heading { display: flex; align-items: center; justify-content: space-between; padding: 27px 18px 16px 20px; } +.eyebrow, .section-kicker { color: #809083; font-size: 9px; font-weight: 800; letter-spacing: .16em; } +.sidebar-heading h1 { margin: 5px 0 0; font-family: 'Newsreader Variable', serif; font-size: 25px; font-weight: 560; letter-spacing: -.035em; } +.search-box { + display: flex; + align-items: center; + gap: 9px; + height: 38px; + margin: 0 14px; + padding: 0 10px; + border: 1px solid #d7ded3; + border-radius: 10px; + color: #809087; + background: rgba(255,255,255,.68); + box-shadow: 0 2px 8px rgba(31,68,51,.025); +} +.search-box:focus-within { border-color: #9db49f; box-shadow: 0 0 0 3px rgba(37,116,82,.08); } +.search-box input { width: 100%; min-width: 0; border: 0; outline: 0; color: var(--ink); background: transparent; font-size: 11px; } +.search-box input::placeholder { color: #94a197; } +.search-box kbd { display: flex; align-items: center; gap: 2px; padding: 3px 5px; border: 1px solid #dce2d8; border-radius: 5px; color: #9aa59d; background: #fafbf8; font-size: 9px; } +.court-count-row { display: flex; justify-content: space-between; padding: 19px 20px 8px; color: #809087; font-size: 9px; text-transform: uppercase; letter-spacing: .06em; } +.legend { display: flex; align-items: center; gap: 5px; } +.legend i { width: 6px; height: 6px; border-radius: 50%; background: #35a16a; box-shadow: 0 0 0 3px rgba(53,161,106,.1); } +.court-list { min-height: 0; flex: 1; overflow-y: auto; padding: 4px 8px; } +.court-row { + position: relative; + display: grid; + grid-template-columns: 42px minmax(0, 1fr) 8px; + align-items: center; + width: 100%; + gap: 11px; + padding: 10px; + border: 1px solid transparent; + border-radius: 13px; + text-align: left; + background: transparent; + cursor: pointer; + transition: transform .16s ease, border-color .16s ease, background .16s ease, box-shadow .16s ease; +} +.court-row:hover { transform: translateY(-1px); background: rgba(255,255,255,.48); } +.court-row.is-selected { border-color: #d9e2d4; background: var(--paper); box-shadow: 0 7px 20px rgba(31,68,51,.07); } +.court-row.is-selected::before { position: absolute; left: -9px; width: 3px; height: 29px; border-radius: 3px; background: var(--green); content: ''; } +.court-monogram { + --court-accent: var(--tennis); + position: relative; + display: grid; + place-items: center; + width: 42px; + height: 42px; + overflow: hidden; + border-radius: 11px; + color: var(--tennis); + background: var(--green); +} +.court-monogram > span { z-index: 1; font-family: 'Newsreader Variable', serif; font-size: 18px; font-weight: 700; } +.court-lines { position: absolute; inset: 7px; border: 1px solid rgba(215,244,92,.3); border-radius: 2px; } +.court-lines::after { position: absolute; top: 50%; left: 0; width: 100%; border-top: 1px solid rgba(215,244,92,.3); content: ''; } +.court-row-copy { display: flex; min-width: 0; flex-direction: column; gap: 4px; } +.court-row-title { display: flex; align-items: center; gap: 6px; overflow: hidden; color: #254236; font-size: 11px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; } +.court-row-title svg { flex: none; color: #809087; } +.court-row-meta { color: #829087; font-size: 9px; } +.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #a1aba3; } +.status-online { background: #35a16a; box-shadow: 0 0 0 3px rgba(53,161,106,.1); } +.status-login-required { background: #d1a047; } +.status-maintenance { background: #a1aba3; } +.sidebar-footer { display: grid; grid-template-columns: 30px 1fr 14px; align-items: center; gap: 9px; margin: 8px; padding: 13px 12px; border-top: 1px solid #d9dfd4; color: #829087; } +.avatar { display: grid; place-items: center; width: 29px; height: 29px; border-radius: 50%; color: var(--tennis); background: var(--green); font-size: 10px; font-weight: 850; } +.sidebar-footer div:nth-child(2) { display: flex; flex-direction: column; gap: 2px; } +.sidebar-footer strong { color: #40594d; font-size: 10px; font-weight: 700; } +.sidebar-footer span { font-size: 8px; } + +.workspace { position: relative; min-width: 0; min-height: 0; overflow: hidden; padding-bottom: 70px; background: var(--canvas); } +.workspace::before { + position: absolute; + top: -180px; + right: -100px; + width: 520px; + height: 520px; + border: 1px solid rgba(23,79,58,.05); + border-radius: 50%; + content: ''; + pointer-events: none; +} +.court-hero { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + min-height: 120px; + padding: 25px 30px 20px; + border-bottom: 1px solid var(--line); + background: rgba(255,254,249,.72); +} +.hero-identity { display: flex; align-items: center; gap: 15px; } +.hero-identity .court-monogram { width: 56px; height: 56px; border-radius: 15px; box-shadow: 0 8px 18px rgba(23,79,58,.15); } +.hero-identity .court-monogram > span { font-size: 24px; } +.hero-title-row { display: flex; align-items: center; gap: 10px; } +.hero-title-row h2 { margin: 0; color: var(--ink); font-family: 'Newsreader Variable', serif; font-size: 30px; font-weight: 560; letter-spacing: -.04em; } +.hero-identity p { display: flex; align-items: center; gap: 5px; margin: 6px 0 0; color: #78887e; font-size: 10px; } +.status-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border: 1px solid var(--line); border-radius: 999px; color: #78887e; background: var(--paper); font-size: 9px; font-weight: 650; } +.badge-online { border-color: #cfe4d4; color: #27764f; background: #f1f9ef; } +.badge-login-required { color: #a77729; } +.hero-stats { display: flex; align-items: center; gap: 22px; } +.hero-stats > div { display: flex; flex-direction: column; gap: 4px; min-width: 75px; padding-right: 22px; border-right: 1px solid var(--line); } +.hero-stats span { color: #819087; font-size: 9px; } +.hero-stats strong { color: #29473a; font-size: 11px; font-weight: 720; } +.refresh-button { display: flex; align-items: center; gap: 6px; height: 34px; padding: 0 12px; border: 1px solid #d5ddd2; border-radius: 9px; color: #496157; background: var(--paper); font-size: 10px; font-weight: 650; cursor: pointer; transition: .15s ease; } +.refresh-button:hover { transform: translateY(-1px); border-color: #aebdaf; color: var(--green); box-shadow: 0 5px 12px rgba(31,68,51,.06); } +.is-spinning { animation: spin .8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +.booking-panel { position: relative; z-index: 1; display: flex; height: calc(100% - 120px); min-height: 0; flex-direction: column; padding: 0 30px; } +.date-strip { display: flex; flex: none; gap: 7px; padding: 16px 0; border-bottom: 1px solid var(--line); } +.date-strip > button { display: flex; min-width: 78px; height: 52px; flex-direction: column; align-items: flex-start; justify-content: center; gap: 3px; padding: 0 12px; border: 1px solid transparent; border-radius: 11px; color: #7c8b82; background: transparent; cursor: pointer; transition: .15s ease; } +.date-strip > button:hover { border-color: #e0e4db; background: rgba(255,255,255,.55); } +.date-strip > button span { font-size: 9px; } +.date-strip > button strong { color: #496157; font-size: 11px; font-weight: 720; } +.date-strip > button.is-selected { border-color: var(--green); color: #4e675a; background: var(--tennis-soft); box-shadow: 0 5px 12px rgba(52,91,43,.06); } +.date-strip > button.is-selected strong { color: var(--green); } +.date-strip .calendar-button { min-width: 48px; align-items: center; color: #667a6e; background: rgba(255,255,255,.45); } +.schedule-toolbar { display: flex; flex: none; align-items: flex-end; justify-content: space-between; padding: 19px 0 12px; } +.section-kicker { display: flex; align-items: center; gap: 5px; color: #728479; } +.section-kicker svg { color: var(--green-bright); } +.schedule-toolbar h3 { margin: 4px 0 0; color: var(--ink); font-family: 'Newsreader Variable', serif; font-size: 23px; font-weight: 560; } +.schedule-legend { display: flex; gap: 15px; color: #718077; font-size: 9px; } +.schedule-legend span { display: flex; align-items: center; gap: 5px; } +.schedule-legend i { width: 7px; height: 7px; border-radius: 2px; } +.schedule-legend .available { background: #56a778; } +.schedule-legend .activity { background: #5d79c7; } +.schedule-legend .booked { background: #dc8e7d; } +.schedule-legend .blocked { background: #c9aa76; } +.schedule-scroll { min-height: 0; overflow: auto; padding: 0 1px 26px; } +.schedule-grid { display: grid; grid-template-columns: 72px repeat(var(--court-columns), minmax(156px, 1fr)); min-width: 1040px; gap: 7px; } +.grid-corner { position: sticky; z-index: 5; top: 0; left: 0; display: flex; align-items: center; gap: 6px; padding: 10px 7px; color: #7b8a81; background: var(--canvas); font-size: 9px; } +.court-column-head { position: sticky; z-index: 4; top: 0; padding: 10px; color: #4f675a; background: var(--canvas); font-size: 9px; font-weight: 750; text-align: center; } +.schedule-row { display: contents; } +.time-label { position: sticky; z-index: 3; left: 0; display: flex; flex-direction: column; justify-content: center; min-height: 96px; padding: 8px 7px; background: var(--canvas); } +.time-label strong { color: #2b493c; font-size: 11px; font-weight: 760; } +.time-label span { margin-top: 2px; color: #929e96; font-size: 8px; } +.slot-cell { + position: relative; + display: flex; + min-width: 0; + min-height: 96px; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 10px; + overflow: hidden; + border: 1px solid #dce3d9; + border-radius: 11px; + background: var(--paper); + box-shadow: 0 2px 8px rgba(31,68,51,.025); + cursor: pointer; + opacity: 1; + transition: transform .14s ease, border-color .14s ease, box-shadow .14s ease; +} +.slot-cell:not(:disabled):hover { transform: translateY(-2px); border-color: #83a88e; box-shadow: 0 7px 16px rgba(31,68,51,.08); } +.slot-cell.is-selected { border-color: var(--green); background: var(--tennis-soft); box-shadow: 0 0 0 2px rgba(23,79,58,.08); } +.slot-price { color: #234d39; font-family: 'Newsreader Variable', serif; font-size: 17px; font-weight: 650; } +.slot-state { flex: none; padding: 3px 6px; border-radius: 999px; color: #27704d; background: #e8f5e9; font-size: 8px; font-weight: 750; } +.slot-limited { border-color: #ead4aa; background: #fffbef; } +.slot-limited .slot-state { color: #9a6e28; background: #f8e8be; } +.slot-cell.has-enrollment { align-items: stretch; border-color: #cfd8f0; background: #f2f5ff; } +.slot-cell.has-enrollment:hover { border-color: #7890cf; box-shadow: 0 7px 18px rgba(54,78,144,.12); } +.slot-cell.has-enrollment.is-selected { border-color: #4e69b4; background: #eaf0ff; box-shadow: 0 0 0 2px rgba(78,105,180,.1); } +.enrollment-card { display: flex; min-width: 0; width: 100%; flex-direction: column; gap: 5px; text-align: left; } +.enrollment-topline { display: flex; align-items: center; justify-content: space-between; gap: 6px; } +.enrollment-tag { padding: 3px 6px; border-radius: 999px; color: #fff; background: #536fb9; font-size: 8px; font-weight: 800; letter-spacing: .04em; } +.enrollment-count { color: #52658f; font-size: 8px; font-weight: 750; } +.enrollment-card > strong { display: -webkit-box; overflow: hidden; color: #293d78; font-size: 10px; font-weight: 800; line-height: 1.3; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } +.enrollment-meta { color: #5a6e9f; font-size: 8px; font-weight: 650; white-space: nowrap; } +.enrollment-players { overflow: hidden; color: #7382a8; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; } +.slot-booked { align-items: flex-start; border-color: #efd4cc; background: var(--coral-soft); cursor: default; } +.slot-booked .slot-state { color: #a84f3e; background: #f9d9d1; } +.booking-contacts { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 5px; } +.booking-contact { display: flex; min-width: 0; flex-direction: column; gap: 2px; text-align: left; } +.booking-contact strong { overflow: hidden; color: #743b30; font-size: 10px; font-weight: 780; text-overflow: ellipsis; white-space: nowrap; } +.booking-contact small { color: #a45d4e; font-size: 9px; font-weight: 620; letter-spacing: .015em; white-space: nowrap; } +.slot-blocked { border-color: #eadfc9; background: var(--sand-soft); cursor: default; } +.slot-blocked .slot-price { color: #876839; font-size: 15px; } +.slot-blocked .slot-state { color: #8a6632; background: #ecddbf; } +.slot-empty { justify-content: center; color: #aeb8b0; background: rgba(255,255,255,.35); box-shadow: none; } +.loading-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; overflow: hidden; } +.loading-grid i { height: 96px; border-radius: 11px; background: linear-gradient(90deg, #eeece5 20%, #faf9f4 50%, #eeece5 80%); background-size: 220% 100%; animation: shimmer 1.4s infinite linear; } +@keyframes shimmer { to { background-position: -220% 0; } } +.error-state { display: flex; align-items: center; gap: 8px; margin: 16px 0; padding: 12px; border: 1px solid #efcfc5; border-radius: 10px; color: #a84f3e; background: var(--coral-soft); font-size: 10px; } +.booking-error span { flex: 1; } +.booking-error button { padding: 3px 7px; border: 1px solid #e2b9ae; border-radius: 6px; color: #9f4c3b; background: transparent; font-size: 8px; cursor: pointer; } +.selection-bar { position: absolute; z-index: 8; right: 22px; bottom: 14px; left: 22px; display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 20px; min-height: 58px; padding: 8px 9px 8px 17px; border: 1px solid #d3ddd1; border-radius: 14px; background: rgba(255,254,249,.96); box-shadow: 0 16px 40px rgba(32,65,50,.14); backdrop-filter: blur(16px); transform: translateY(90px); opacity: 0; transition: transform .22s ease, opacity .22s ease; } +.selection-bar.is-visible { transform: translateY(0); opacity: 1; } +.selection-summary { display: flex; flex-direction: column; gap: 3px; } +.selection-summary span { color: #829087; font-size: 8px; text-transform: uppercase; letter-spacing: .08em; } +.selection-summary strong { color: #2a493b; font-size: 11px; font-weight: 720; } +.selection-price { color: var(--green); font-family: 'Newsreader Variable', serif; font-size: 21px; font-weight: 650; } +.selection-bar button { height: 40px; padding: 0 17px; border: 0; border-radius: 10px; color: var(--tennis); background: var(--green); font-size: 10px; font-weight: 780; cursor: pointer; box-shadow: 0 6px 14px rgba(23,79,58,.17); } +.selection-bar button:disabled { cursor: not-allowed; opacity: .48; } +.pending-booking-bar { position: absolute; z-index: 8; right: 22px; bottom: 14px; left: 22px; display: grid; grid-template-columns: 38px 1fr auto auto; align-items: center; gap: 14px; min-height: 68px; padding: 9px 10px 9px 13px; border: 1px solid #cbd9ca; border-radius: 14px; background: rgba(249,253,242,.97); box-shadow: 0 16px 40px rgba(32,65,50,.14); backdrop-filter: blur(16px); } +.pending-lock-icon { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 10px; color: var(--tennis); background: var(--green); } +.pending-booking-copy { display: flex; min-width: 0; flex-direction: column; gap: 3px; } +.pending-booking-copy > span { color: #2d7b52; font-size: 8px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } +.pending-booking-copy strong { color: #27473a; font-size: 11px; font-weight: 750; } +.pending-booking-copy small { overflow: hidden; color: #7d8c83; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; } +.release-booking-button { display: flex; align-items: center; gap: 6px; height: 40px; padding: 0 13px; border: 1px solid #e4bfb6; border-radius: 10px; color: #a44f3d; background: #fff5f1; font-size: 9px; font-weight: 750; cursor: pointer; } +.release-booking-button:hover:not(:disabled) { border-color: #cc8f80; background: #ffede7; } +.release-booking-button:disabled { cursor: not-allowed; opacity: .5; } +.empty-workspace { display: grid; height: 100%; place-content: center; justify-items: center; gap: 10px; color: #78887e; font-size: 11px; } +.empty-error { color: #a84f3e; } +.empty-error strong { color: #743b30; font-family: 'Newsreader Variable', serif; font-size: 22px; font-weight: 600; } +.empty-error span { color: #8b625a; } +.fatal-error { display: grid; min-height: 100vh; place-content: center; justify-items: start; padding: 48px; background: var(--canvas); } +.fatal-error span { color: var(--green-bright); font-size: 9px; font-weight: 800; letter-spacing: .16em; } +.fatal-error h1 { margin: 8px 0; color: var(--ink); font-family: 'Newsreader Variable', serif; font-size: 32px; font-weight: 560; } +.fatal-error p { max-width: 640px; margin: 0 0 8px; color: #a84f3e; font-size: 12px; } +.fatal-error small { color: #78887e; font-size: 10px; } + +.settings-backdrop { + position: fixed; + z-index: 100; + inset: 0; + display: grid; + place-items: center; + padding: 28px; + background: rgba(32, 51, 41, .18); + backdrop-filter: blur(6px); + animation: fade-in .16s ease-out; +} +.settings-dialog { + width: min(440px, 100%); + padding: 24px; + border: 1px solid #d8dfd4; + border-radius: 18px; + background: var(--paper); + box-shadow: 0 28px 80px rgba(26, 55, 42, .22); + animation: dialog-in .2s ease-out; +} +.settings-dialog-head { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; } +.settings-dialog-head h2 { margin: 3px 0 0; color: var(--ink); font-family: 'Newsreader Variable', serif; font-size: 25px; font-weight: 600; letter-spacing: -.03em; } +.settings-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 12px; color: var(--tennis); background: var(--green); box-shadow: 0 7px 16px rgba(23,79,58,.16); } +.settings-court-card { display: flex; flex-direction: column; gap: 4px; padding: 14px 15px; border: 1px solid #dfe6dc; border-radius: 12px; background: #f4f7f0; } +.settings-court-card span { color: #809087; font-size: 8px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } +.settings-court-card strong { color: #29473a; font-size: 12px; font-weight: 760; } +.settings-court-card small { color: #7e8e84; font-size: 9px; } +.settings-field { display: flex; flex-direction: column; gap: 7px; margin-top: 18px; } +.settings-field > span { color: #40594d; font-size: 10px; font-weight: 760; } +.settings-field-title { display: flex; align-items: center; justify-content: space-between; } +.settings-field-title i { padding: 3px 6px; border-radius: 999px; color: #a35a4b; background: #f9ded7; font-size: 8px; font-style: normal; font-weight: 750; } +.settings-field-title i.is-configured { color: #28744f; background: #e2f2e4; } +.settings-field input { width: 100%; height: 43px; padding: 0 12px; border: 1px solid #ccd6ca; border-radius: 10px; outline: none; color: var(--ink); background: #fff; font-size: 13px; font-weight: 650; letter-spacing: .02em; transition: .15s ease; } +.settings-field input:focus { border-color: #789981; box-shadow: 0 0 0 3px rgba(37,116,82,.09); } +.settings-field input:disabled { color: #87948b; background: #f2f3ef; } +.settings-field small { color: #86938b; font-size: 9px; line-height: 1.5; } +.settings-field > .credential-input-wrap { position: relative; display: block; color: inherit; font-size: inherit; font-weight: inherit; } +.credential-input-wrap input { padding-right: 43px; } +.credential-input-wrap button { position: absolute; top: 50%; right: 7px; display: grid; width: 30px; height: 30px; padding: 0; border: 0; border-radius: 8px; place-items: center; color: #6e7f74; background: transparent; transform: translateY(-50%); cursor: pointer; transition: color .15s ease, background .15s ease; } +.credential-input-wrap button:hover:not(:disabled) { color: #225c42; background: #edf3ec; } +.credential-input-wrap button:focus-visible { outline: 2px solid #789981; outline-offset: 1px; } +.credential-input-wrap button:disabled { cursor: default; opacity: .35; } +.settings-loading { display: grid; min-height: 94px; place-items: center; color: #7e8e84; font-size: 10px; } +.settings-error { display: flex; align-items: center; gap: 7px; margin-top: 13px; padding: 10px; border: 1px solid #efcfc5; border-radius: 9px; color: #a84f3e; background: var(--coral-soft); font-size: 9px; } +.settings-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; padding-top: 16px; border-top: 1px solid #e5e9e1; } +.settings-actions button { height: 37px; padding: 0 14px; border: 1px solid #d6ddd3; border-radius: 9px; color: #63756b; background: #fff; font-size: 10px; font-weight: 720; cursor: pointer; } +.settings-actions button:hover:not(:disabled) { border-color: #aebcaf; color: var(--green); } +.settings-actions .save-settings-button { min-width: 104px; border-color: var(--green); color: var(--tennis); background: var(--green); box-shadow: 0 6px 14px rgba(23,79,58,.14); } +.settings-actions button:disabled { cursor: not-allowed; opacity: .48; } +.booking-confirm-dialog { width: min(430px, 100%); padding: 27px; border: 1px solid #d8dfd4; border-radius: 19px; background: var(--paper); box-shadow: 0 28px 80px rgba(26,55,42,.22); text-align: center; animation: dialog-in .2s ease-out; } +.confirm-lock-icon { display: grid; place-items: center; width: 46px; height: 46px; margin: 0 auto 13px; border-radius: 14px; color: var(--tennis); background: var(--green); box-shadow: 0 8px 18px rgba(23,79,58,.17); } +.booking-confirm-dialog h2 { margin: 6px 0 7px; color: var(--ink); font-family: 'Newsreader Variable', serif; font-size: 25px; font-weight: 600; letter-spacing: -.035em; } +.booking-confirm-dialog > p { max-width: 330px; margin: 0 auto; color: #75857c; font-size: 10px; line-height: 1.65; } +.confirm-booking-summary { display: grid; gap: 0; margin-top: 20px; overflow: hidden; border: 1px solid #dfe6dc; border-radius: 12px; background: #f6f8f3; text-align: left; } +.confirm-booking-summary > div { display: grid; grid-template-columns: 58px 1fr; gap: 10px; align-items: center; min-height: 39px; padding: 7px 12px; border-bottom: 1px solid #e3e8e0; } +.confirm-booking-summary > div:last-child { border-bottom: 0; } +.confirm-booking-summary span { color: #839188; font-size: 9px; } +.confirm-booking-summary strong { overflow: hidden; color: #345044; font-size: 10px; font-weight: 720; text-overflow: ellipsis; white-space: nowrap; } +.booking-confirm-dialog .settings-actions { justify-content: center; } +.booking-confirm-dialog .settings-actions button { min-width: 112px; } +.release-dialog .confirm-lock-icon { color: #fff1ec; background: var(--coral); } +.settings-actions .danger-action-button { min-width: 125px; border-color: var(--coral); color: #fff; background: var(--coral); box-shadow: 0 6px 14px rgba(185,91,72,.18); } +@keyframes fade-in { from { opacity: 0; } } +@keyframes dialog-in { from { transform: translateY(8px) scale(.985); opacity: 0; } } + +::-webkit-scrollbar { width: 9px; height: 9px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: 9px; background: #c9d1c8; background-clip: padding-box; } +::-webkit-scrollbar-thumb:hover { background: #aeb9af; background-clip: padding-box; } + +@media (max-width: 1180px) { + .app-shell { grid-template-columns: 250px minmax(0, 1fr); } + .hero-stats > div:first-child { display: none; } + .date-strip > button { min-width: 68px; padding: 0 9px; } + .schedule-grid { grid-template-columns: 68px repeat(var(--court-columns), minmax(150px, 1fr)); } +} diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts new file mode 100644 index 0000000..c8fe560 --- /dev/null +++ b/src/shared/contracts.ts @@ -0,0 +1,122 @@ +export type CourtStatus = 'online' | 'maintenance' | 'login-required' + +export interface CourtSummary { + id: string + name: string + shortName: string + district?: string + address?: string + distanceKm?: number + surface?: string + indoor?: boolean + accent: string + status: CourtStatus +} + +export type SlotStatus = 'available' | 'limited' | 'booked' | 'blocked' + +export interface BookingContact { + name?: string + phone?: string +} + +export interface EnrollmentActivity { + id: string + title: string + startTime: string + endTime: string + feeCents: number + enrolledCount: number + capacity: number + participantNames: string[] + enrollmentDeadline?: string +} + +export interface BookingSlot { + id: string + courtLabel: string + startTime: string + endTime: string + priceCents: number + status: SlotStatus + unavailableLabel?: string + bookedBy?: BookingContact[] + enrollment?: EnrollmentActivity + providerReference?: { + classroomUid: string + beginDatetime: string + endDatetime: string + } + sourceLabel?: string +} + +export interface AvailabilityDay { + courtId: string + date: string + updatedAt: string + slots: BookingSlot[] +} + +export interface AvailabilityQuery { + courtId: string + date: string +} + +export interface CourtSettings { + courtId: string + visitorId?: string + visitorIdConfigured: boolean +} + +export interface UpdateCourtSettingsInput { + courtId: string + visitorId?: string +} + +export interface CreateBookingInput { + courtId: string + date: string + slotId: string + expectedPriceCents: number +} + +export interface CancelBookingInput { + courtId: string + orderId: string +} + +export interface PendingBooking { + courtId: string + orderId: string + venueAppointmentIds: string[] + amountCents: number + courtLabel: string + startTime: string + endTime: string + expiresAt?: string + status: + | 'submitting-uncertain' + | 'pending-payment' + | 'release-uncertain' + | 'release-confirmed' +} + +export interface TennisBookApi { + listCourts(): Promise + getAvailability(query: AvailabilityQuery): Promise + getCourtSettings(courtId: string): Promise + updateCourtSettings(input: UpdateCourtSettingsInput): Promise + createBooking(input: CreateBookingInput): Promise + cancelBooking(input: CancelBookingInput): Promise + getPendingBooking(courtId: string): Promise +} + +export const IPC_CHANNELS = { + listCourts: 'courts:list', + getAvailability: 'courts:availability', + getCourtSettings: 'court-settings:get', + updateCourtSettings: 'court-settings:update', + createBooking: 'bookings:create', + cancelBooking: 'bookings:cancel', + getPendingBooking: 'bookings:pending' +} as const diff --git a/src/shared/global.d.ts b/src/shared/global.d.ts new file mode 100644 index 0000000..2088093 --- /dev/null +++ b/src/shared/global.d.ts @@ -0,0 +1,9 @@ +import type { TennisBookApi } from './contracts' + +declare global { + interface Window { + tennisBook: TennisBookApi + } +} + +export {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..76d3729 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "jsx": "react-jsx", + "types": ["node", "vite/client"] + }, + "include": ["electron.vite.config.ts", "src/**/*.ts", "src/**/*.tsx"] +}