commit 1f104f73c82d5ba1d85e0570fea190070678d619 Author: ssdfasd <2156608475@qq.com> Date: Sun Mar 8 01:34:54 2026 +0800 Initial commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7f9debd --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run:*)", + "Bash(curl:*)" + ] + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..aa13c6c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,52 @@ +--- +name: 🐛 Bug 报告 +about: 报告一个 Bug 帮助我们改进 +title: '[Bug] ' +labels: bug +assignees: '' +--- + +## Bug 描述 + +请简洁清晰地描述这个 Bug 是什么。 + +## 复现步骤 + +请提供详细的复现步骤: + +1. 进入 '...' +2. 点击 '...' +3. 执行 '...' +4. 出现错误 + +## 预期行为 + +请描述你期望发生的正确行为。 + +## 实际行为 + +请描述实际发生的错误行为。 + +## 截图 + +如果可以,请提供截图帮助说明问题。 + +## 环境信息 + +- 操作系统: [例如 Windows 11] +- 应用版本: [例如 0.0.1] +- 浏览器版本: [如果适用] + +## 附加信息 + +添加任何其他有用的信息,如日志、堆栈跟踪等。 + +``` + +``` + +## 检查清单 + +- [ ] 我已经搜索过类似的问题,确认没有重复 Issue +- [ ] 我提供了完整的复现步骤 +- [ ] 我提供了环境信息 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e12c858 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,33 @@ +--- +name: 💡 功能请求 +about: 为项目提出新功能或改进建议 +title: '[Feature] ' +labels: enhancement +assignees: '' +--- + +## 功能描述 + +请简洁清晰地描述你希望添加的功能或改进。 + +## 解决的问题 + +这个功能将解决什么问题?或者会带来什么价值? + +## 建议的解决方案 + +请描述你建议如何实现这个功能。 + +## 替代方案 + +请描述你考虑过的其他替代方案。 + +## 设计稿 + +如果有设计想法,请提供草图或描述。 + +## 检查清单 + +- [ ] 我已经搜索过类似的功能请求,确认没有重复 Issue +- [ ] 我清晰地描述了期望的功能 +- [ ] 我解释了为什么需要这个功能 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5fef97e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,35 @@ +## 描述 + +请简要描述这个 Pull Request 解决了什么问题或添加了什么功能。 + +## 变更类型 + +- [ ] 🐛 Bug 修复 +- [ ] 💡 新功能 +- [ ] 📝 文档更新 +- [ ] ♻️ 代码重构 +- [ ] ⚡ 性能优化 +- [ ] 🔧 构建或工具变动 + +## 如何测试 + +请描述如何验证这些变更是否正常工作。 + +## 检查清单 + +- [ ] 我的代码遵循项目的代码规范 +- [ ] 我已经进行了自测 +- [ ] 我已经更新了相关文档(如果需要) +- [ ] 我的变更没有引入新的警告或错误 + +## 截图(可选) + +如果适用,请提供变更前后的对比截图。 + +--- + +**关联的 Issue**: 关闭 #(如果有) + +## 其他信息 + +添加任何其他需要审查者注意的信息。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a6aecc --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.vite + +# Build output +release/ + +# Tools output +tools/tongyi/ppt_output/ +tools/tongyi/downloads/ +tools/tongyi/__pycache__/ +tools/mineru/__pycache__/ +tools/blog/__pycache__/ + +# Notebook pydemos backup +notebook/ \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..34862ff --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +electron_mirror=https://npmmirror.com/mirrors/electron/ +electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..7c1a22c --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectName":"trae_byb2803z"} \ No newline at end of file diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..a48e5d3 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,7 @@ +node_modules +build +dist +.git +.trae +.log +.figma \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c5c275f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,202 @@ +# 贡献指南 + +感谢你对 XCNote 项目的兴趣!我们欢迎任何形式的贡献,包括但不限于: + +- 🐛 报告 Bug +- 💡 提出新功能建议 +- 📝 完善文档 +- 💻 提交代码修复或新功能 +- 🔧 改进构建流程 + +## 开发环境搭建 + +### 前置要求 + +- Node.js 18+ +- npm 9+ +- Git + +### 克隆项目 + +```bash +git clone https://github.com/your-repo/XCNote.git +cd XCNote +``` + +### 安装依赖 + +```bash +npm install +``` + +### 启动开发服务器 + +```bash +# 启动前端开发服务器 +npm run dev + +# 或启动完整 Electron 开发环境(推荐) +npm run electron:dev +``` + +### 代码检查 + +```bash +# 运行 ESLint +npm run lint + +# 运行 TypeScript 类型检查 +npm run check +``` + +## 代码规范 + +### 命名规范 + +- **文件命名**: 使用 kebab-case(如 `file-system.tsx`) +- **组件命名**: 使用 PascalCase(如 `FileTree.tsx`) +- **函数命名**: 使用 camelCase(如 `useFileTree`) +- **常量命名**: 使用 UPPER_SNAKE_CASE + +### TypeScript 规范 + +- 尽量使用明确的类型定义,避免使用 `any` +- 优先使用接口(interface)而不是类型别名(type)定义对象结构 +- 导出类型时使用 `export type` + +### React 规范 + +- 使用函数组件和 Hooks +- 组件文件以 `.tsx` 为扩展名 +- 纯逻辑文件以 `.ts` 为扩展名 + +### CSS 规范 + +- 使用 Tailwind CSS 进行样式开发 +- 避免直接编写 CSS,优先使用 Tailwind 工具类 +- 自定义样式放在组件同名的 `.module.css` 文件中 + +### Git 提交规范 + +我们使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +``` +(): + +[optional body] + +[optional footer] +``` + +#### 类型(type) + +| 类型 | 说明 | +|------|------| +| feat | 新功能 | +| fix | Bug 修复 | +| docs | 文档更新 | +| style | 代码格式调整 | +| refactor | 代码重构 | +| perf | 性能优化 | +| test | 测试相关 | +| chore | 构建或辅助工具变动 | + +#### 示例 + +```bash +# 新功能 +git commit -m "feat(file-system): 添加文件拖拽功能" + +# Bug 修复 +git commit -m "fix(editor): 修复数学公式渲染错误" + +# 文档更新 +git commit -m "docs: 更新 README 安装步骤" +``` + +## Pull Request 流程 + +### 创建分支 + +```bash +# 从主分支创建新分支 +git checkout -b feature/your-feature-name + +# 或创建修复分支 +git checkout -b fix/bug-description +``` + +### 提交代码 + +```bash +# 暂存修改 +git add . + +# 提交修改 +git commit -m "feat(module): 添加新功能" +``` + +### 推送分支 + +```bash +# 推送到远程仓库 +git push origin feature/your-feature-name +``` + +### 创建 Pull Request + +1. 访问项目仓库 +2. 点击 "Compare & pull request" +3. 填写 PR 标题和描述 +4. 提交 PR + +### PR 标题规范 + +请使用与提交信息相同的格式: + +``` +feat(file-system): 添加文件搜索功能 +fix(editor): 修复保存内容丢失问题 +``` + +## 项目结构概览 + +``` +XCNote/ +├── src/ # 前端源码 +│ ├── components/ # UI 组件 +│ ├── contexts/ # React Context +│ ├── hooks/ # 自定义 Hooks +│ ├── lib/ # 工具库 +│ ├── modules/ # 功能模块 +│ └── pages/ # 页面组件 +├── api/ # 后端 API +├── electron/ # Electron 主进程 +├── shared/ # 共享类型定义 +└── notebook/ # 笔记存储目录 +``` + +### 添加新模块 + +如需添加新功能模块,请参考以下步骤: + +1. 在 `src/modules/` 下创建新模块目录 +2. 在 `shared/modules/` 下定义模块配置 +3. 在后端 `api/modules/` 下实现 API 接口 +4. 使用模块注册系统进行注册 + +## 问题反馈 + +如果你发现了 Bug 或有新功能建议,请: + +1. 搜索是否已有类似问题 +2. 如果没有,创建新的 Issue +3. 使用对应的模板并提供详细信息 + +## 行为准则 + +请尊重所有参与项目的开发者,保持友好和专业的交流环境。 + +--- + +感谢你的贡献!🎉 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbc3fb3 --- /dev/null +++ b/README.md @@ -0,0 +1,193 @@ +# XCDesktop + +
+ +[![Electron](https://img.shields.io/badge/Electron-40.2.1-blue?style=flat-square)](https://www.electronjs.org/) +[![React](https://img.shields.io/badge/React-18.3.1-61DAFB?style=flat-square)](https://react.dev/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-3178C6?style=flat-square)](https://www.typescriptlang.org/) +[![Vite](https://img.shields.io/badge/Vite-6.3.5-646CFF?style=flat-square)](https://vitejs.dev/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3.4.17-06B6D4?style=flat-square)](https://tailwindcss.com/) + +一站式 AI 工作台 - 集成笔记管理、时间追踪、任务管理、AI 辅助等多种功能 + +
+ +## ✨ 核心特性 + +- **🤖 AI 集成** - 深度集成通义万相、豆包等 AI 能力,支持语音转文字、视频解析、PPT 提取等 +- **📝 Markdown 笔记管理** - 基于 Milkdown 编辑器,支持数学公式(KaTeX)、代码高亮(Prism)、表格、任务列表等 +- **⏱️ 时间追踪** - 记录学习和工作时间,生成生产力统计图表 +- **✅ 任务管理** - 完善的 TODO 功能,支持时间规划和任务统计 +- **🗂️ 文件管理** - 树形文件结构,支持拖拽、重命名、删除等操作 +- **🔍 全文搜索** - 快速搜索笔记内容 +- **🌐 远程网页** - 内置浏览器,集成微信读书、B 站等常用网站 +- **🧩 模块化架构** - 前端模块热插拔,易于扩展新功能 +- **🖥️ 桌面原生体验** - Electron 打包,离线可用,支持系统主题 + +## 🚀 快速开始 + +### 环境要求 + +- Node.js 18+ +- npm 9+ +- Windows 10+ + +### 安装依赖 + +```bash +npm install +``` + +### 运行开发模式 + +```bash +# 启动前端开发服务器 +npm run dev + +# 或者使用 Electron 开发模式(推荐) +npm run electron:dev +``` + +### 构建项目 + +```bash +# 构建前端 +npm run build + +# 构建 Electron 应用 +npm run electron:build +``` + +构建完成后,安装包位于 `release` 目录下。 + +## 📁 项目结构 + +``` +XCDesktop/ +├── src/ # 前端源代码 +│ ├── components/ # React 组件 +│ ├── contexts/ # React Context +│ ├── hooks/ # 自定义 Hooks +│ │ ├── domain/ # 业务逻辑 Hooks +│ │ ├── events/ # 事件处理 +│ │ ├── ui/ # UI 交互 Hooks +│ │ └── utils/ # 工具函数 +│ ├── lib/ # 工具库 +│ │ ├── api/ # API 客户端 +│ │ ├── editor/ # 编辑器配置 +│ │ └── utils/ # 工具函数 +│ ├── modules/ # 功能模块 +│ │ ├── home/ # 首页 +│ │ ├── settings/ # 设置 +│ │ ├── search/ # 搜索 +│ │ ├── todo/ # 任务管理 +│ │ ├── time-tracking/ # 时间追踪 +│ │ ├── recycle-bin/ # 回收站 +│ │ ├── pydemos/ # Python Demo +│ │ ├── weread/ # 微信读书 +│ │ └── remote/ # 远程网页 +│ ├── stores/ # Zustand 状态管理 +│ └── types/ # 类型定义 +├── api/ # 后端 API (Express) +│ ├── config/ # 配置文件 +│ ├── core/ # 核心功能 +│ ├── errors/ # 错误处理 +│ ├── modules/ # API 模块 +│ │ ├── ai/ # AI 集成 +│ │ ├── document-parser/ # 文档解析 +│ │ ├── pydemos/ # Python Demo +│ │ ├── recycle-bin/ # 回收站 +│ │ ├── remote/ # 远程网页 +│ │ ├── time-tracking/ # 时间追踪 +│ │ └── todo/ # 任务管理 +│ ├── middlewares/ # 中间件 +│ ├── schemas/ # 数据验证 +│ ├── utils/ # 工具函数 +│ ├── watcher/ # 文件监控 +│ └── events/ # 事件总线 +├── electron/ # Electron 主进程 +├── shared/ # 共享类型和配置 +├── tools/ # 工具脚本 +│ └── tongyi/ # 通义万相 AI 工具 +├── notebook/ # 笔记数据存储(运行时) +└── release/ # 构建输出 +``` + +## 🧩 功能模块 + +### 核心模块 + +| 模块 | 功能描述 | +|------|----------| +| 首页 | 欢迎页面,快速访问常用功能 | +| 文件管理 | 树形目录结构,管理 Markdown 笔记 | +| 编辑器 | 基于 Milkdown 的 Markdown 编辑器 | +| 搜索 | 全文搜索笔记内容 | +| 设置 | 主题、壁纸、字体大小等个性化配置 | + +### 扩展模块 + +| 模块 | 功能描述 | +|------|----------| +| AI 集成 | 通义万相语音转文字、视频解析、PPT 提取等 | +| 文档解析 | 支持导入博客、PDF 等格式 | +| 时间追踪 | 记录工作/学习时间,统计生产力 | +| 任务管理 | TODO 列表,支持时间规划 | +| 回收站 | 误删恢复 | +| Python Demo | Python 脚本管理 | +| 微信读书 | 微信读书网页版集成 | +| 远程网页 | 内置浏览器,访问任意网页 | + +## 🛠️ 技术栈 + +### 前端 + +- **框架**: React 18 + TypeScript +- **构建工具**: Vite 6 +- **样式**: Tailwind CSS + Tailwind Typography +- **编辑器**: Milkdown (Markdown) +- **图表**: Recharts +- **图标**: Lucide React +- **路由**: React Router DOM +- **状态管理**: Zustand +- **拖拽**: @dnd-kit + +### 后端 + +- **运行时**: Node.js +- **框架**: Express +- **验证**: Zod +- **文件监控**: Chokidar + +### 桌面 + +- **框架**: Electron 40 +- **日志**: electron-log + +### AI 工具 + +- **语音识别**: 通义听悟 +- **视频解析**: Bilibili 视频下载与分析 +- **PPT 提取**: 自动提取 PPT 内容 + +## 📝 数据存储 + +数据文件存储在系统文档目录下的 `XCDesktop` 文件夹中: + +``` +~/Documents/XCDesktop/ +├── notebook/ +│ ├── markdowns/ # Markdown 笔记 +│ ├── pydemos/ # Python Demo +│ ├── images/ # 图片资源 +│ └── time/ # 时间追踪数据 +└── downloads/ # 下载文件 +``` + +## 🤝 贡献指南 + +欢迎提交 Issue 和 Pull Request!请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。 + +## 📄 许可证 + +MIT License diff --git a/api/app.ts b/api/app.ts new file mode 100644 index 0000000..718e6a7 --- /dev/null +++ b/api/app.ts @@ -0,0 +1,101 @@ +/** + * API 服务器 + */ + +import express, { + type Request, + type Response, + type NextFunction, +} from 'express' +import cors from 'cors' +import dotenv from 'dotenv' +import filesRoutes from './core/files/routes.js' +import eventsRoutes from './core/events/routes.js' +import settingsRoutes from './core/settings/routes.js' +import uploadRoutes from './core/upload/routes.js' +import searchRoutes from './core/search/routes.js' +import type { ApiResponse } from '../shared/types.js' +import { errorHandler } from './middlewares/errorHandler.js' +import { NOTEBOOK_ROOT } from './config/paths.js' +import { ModuleManager } from './infra/moduleManager.js' +import { ServiceContainer } from './infra/container.js' +import { apiModules } from './modules/index.js' +import { validateModuleConsistency } from './infra/moduleValidator.js' +import path from 'path' +import fs from 'fs' + +dotenv.config() + +const app: express.Application = express() +export const container = new ServiceContainer() +export const moduleManager = new ModuleManager(container) + +app.use(cors()) +app.use(express.json({ limit: '200mb' })) +app.use(express.urlencoded({ extended: true, limit: '200mb' })) + +/** + * Core Routes + */ +app.use('/api/files', filesRoutes) +app.use('/api/events', eventsRoutes) +app.use('/api/settings', settingsRoutes) +app.use('/api/upload', uploadRoutes) +app.use('/api/search', searchRoutes) + +/** + * Module Routes (loaded dynamically via ModuleManager) + */ +for (const module of apiModules) { + await moduleManager.register(module) +} + +await validateModuleConsistency(apiModules) + +for (const module of moduleManager.getAllModules()) { + await moduleManager.activate(module.metadata.id) + const router = await module.createRouter(container) + app.use('/api' + module.metadata.basePath, router) +} + +app.get('/background.png', (req, res, next) => { + const customBgPath = path.join(NOTEBOOK_ROOT, '.config', 'background.png') + if (fs.existsSync(customBgPath)) { + res.sendFile(customBgPath) + } else { + next() + } +}) + +/** + * health + */ +app.use( + '/api/health', + (_req: Request, res: Response): void => { + const response: ApiResponse<{ message: string }> = { + success: true, + data: { message: 'ok' }, + } + res.status(200).json(response) + }, +) + +/** + * 404 handler + */ +app.use((req: Request, res: Response, next: NextFunction) => { + if (req.path.startsWith('/api')) { + const response: ApiResponse = { + success: false, + error: { code: 'NOT_FOUND', message: 'API不存在' }, + } + res.status(404).json(response) + } else { + next() + } +}) + +app.use(errorHandler) + +export default app diff --git a/api/config/index.ts b/api/config/index.ts new file mode 100644 index 0000000..e12c347 --- /dev/null +++ b/api/config/index.ts @@ -0,0 +1,47 @@ +import path from 'path' +import { fileURLToPath } from 'url' +import os from 'os' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export const config = { + get projectRoot(): string { + if (__dirname.includes('app.asar')) { + return path.resolve(__dirname, '..').replace('app.asar', 'app.asar.unpacked') + } + return path.resolve(__dirname, '../../') + }, + + get notebookRoot(): string { + return process.env.NOTEBOOK_ROOT + ? path.resolve(process.env.NOTEBOOK_ROOT) + : path.join(this.projectRoot, 'notebook') + }, + + get tempRoot(): string { + return path.join(os.tmpdir(), 'xcnote_uploads') + }, + + get serverPort(): number { + return parseInt(process.env.PORT || '3001', 10) + }, + + get isVercel(): boolean { + return !!process.env.VERCEL + }, + + get isElectron(): boolean { + return __dirname.includes('app.asar') + }, + + get isDev(): boolean { + return !this.isElectron && !this.isVercel + }, +} + +export const PATHS = { + get PROJECT_ROOT() { return config.projectRoot }, + get NOTEBOOK_ROOT() { return config.notebookRoot }, + get TEMP_ROOT() { return config.tempRoot }, +} diff --git a/api/config/paths.ts b/api/config/paths.ts new file mode 100644 index 0000000..10a7c82 --- /dev/null +++ b/api/config/paths.ts @@ -0,0 +1,7 @@ +import { PATHS, config } from './index.js' + +export { PATHS, config } + +export const PROJECT_ROOT = PATHS.PROJECT_ROOT +export const NOTEBOOK_ROOT = PATHS.NOTEBOOK_ROOT +export const TEMP_ROOT = PATHS.TEMP_ROOT diff --git a/api/core/.gitkeep b/api/core/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/core/events/routes.ts b/api/core/events/routes.ts new file mode 100644 index 0000000..eea35c8 --- /dev/null +++ b/api/core/events/routes.ts @@ -0,0 +1,32 @@ +import express, { Request, Response } from 'express' +import type { ApiResponse } from '../../../shared/types.js' +import { eventBus } from '../../events/eventBus.js' + +const router = express.Router() + +router.get('/', (req: Request, res: Response) => { + if (process.env.VERCEL) { + const response: ApiResponse = { + success: false, + error: { code: 'SSE_UNSUPPORTED', message: 'SSE在无服务器运行时中不受支持' }, + } + return res.status(501).json(response) + } + + const headers = { + 'Content-Type': 'text/event-stream', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache' + } + res.writeHead(200, headers) + + res.write(`data: ${JSON.stringify({ event: 'connected' })}\n\n`) + + eventBus.addClient(res) + + req.on('close', () => { + eventBus.removeClient(res) + }) +}) + +export default router diff --git a/api/core/files/routes.ts b/api/core/files/routes.ts new file mode 100644 index 0000000..ec7157f --- /dev/null +++ b/api/core/files/routes.ts @@ -0,0 +1,280 @@ +import express, { type Request, type Response } from 'express' +import fs from 'fs/promises' +import path from 'path' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import type { FileItemDTO, PathExistsDTO } from '../../../shared/types.js' +import { toPosixPath } from '../../../shared/utils/path.js' +import { pad2 } from '../../../shared/utils/date.js' +import { validateBody, validateQuery } from '../../middlewares/validate.js' +import { + listFilesQuerySchema, + contentQuerySchema, + rawQuerySchema, + saveFileSchema, + pathSchema, + renameSchema, + createDirSchema, + createFileSchema, +} from '../../schemas/index.js' +import { + NotFoundError, + NotADirectoryError, + BadRequestError, + AlreadyExistsError, + ForbiddenError, + ResourceLockedError, + InternalError, + isNodeError, +} from '../../../shared/errors/index.js' +import { logger } from '../../utils/logger.js' + +const router = express.Router() + +router.get( + '/', + validateQuery(listFilesQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const relPath = req.query.path as string + const { safeRelPath, fullPath } = resolveNotebookPath(relPath) + + try { + await fs.access(fullPath) + } catch { + throw new NotFoundError('路径不存在') + } + + const stats = await fs.stat(fullPath) + if (!stats.isDirectory()) { + throw new NotADirectoryError() + } + + const files = await fs.readdir(fullPath) + const items = await Promise.all( + files.map(async (name): Promise => { + const filePath = path.join(fullPath, name) + try { + const fileStats = await fs.stat(filePath) + return { + name, + type: fileStats.isDirectory() ? 'dir' : 'file', + size: fileStats.size, + modified: fileStats.mtime.toISOString(), + path: toPosixPath(path.join(safeRelPath, name)), + } + } catch { + return null + } + }), + ) + + const visibleItems = items.filter((i): i is FileItemDTO => i !== null && !i.name.startsWith('.')) + visibleItems.sort((a, b) => { + if (a.type === b.type) return a.name.localeCompare(b.name) + return a.type === 'dir' ? -1 : 1 + }) + + successResponse(res, { items: visibleItems }) + }), +) + +router.get( + '/content', + validateQuery(contentQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const relPath = req.query.path as string + const { fullPath } = resolveNotebookPath(relPath) + const stats = await fs.stat(fullPath).catch(() => { + throw new NotFoundError('文件不存在') + }) + + if (!stats.isFile()) throw new BadRequestError('不是文件') + + const content = await fs.readFile(fullPath, 'utf-8') + successResponse(res, { + content, + metadata: { + size: stats.size, + modified: stats.mtime.toISOString(), + }, + }) + }), +) + +router.get( + '/raw', + validateQuery(rawQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const relPath = req.query.path as string + const { fullPath } = resolveNotebookPath(relPath) + const stats = await fs.stat(fullPath).catch(() => { + throw new NotFoundError('文件不存在') + }) + + if (!stats.isFile()) throw new BadRequestError('不是文件') + + const ext = path.extname(fullPath).toLowerCase() + const mimeTypes: Record = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + } + + const mimeType = mimeTypes[ext] + if (mimeType) res.setHeader('Content-Type', mimeType) + res.sendFile(fullPath) + }), +) + +router.post( + '/save', + validateBody(saveFileSchema), + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath, content } = req.body + const { fullPath } = resolveNotebookPath(relPath) + await fs.mkdir(path.dirname(fullPath), { recursive: true }) + await fs.writeFile(fullPath, content, 'utf-8') + + successResponse(res, null) + }), +) + +router.delete( + '/delete', + validateBody(pathSchema), + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath } = req.body + const { fullPath } = resolveNotebookPath(relPath) + + await fs.stat(fullPath).catch(() => { + throw new NotFoundError('文件或目录不存在') + }) + + const { fullPath: rbDir } = resolveNotebookPath('RB') + + await fs.mkdir(rbDir, { recursive: true }) + + const originalName = path.basename(fullPath) + const now = new Date() + const year = now.getFullYear() + const month = pad2(now.getMonth() + 1) + const day = pad2(now.getDate()) + const timestamp = `${year}${month}${day}` + const newName = `${timestamp}_${originalName}` + const rbDestPath = path.join(rbDir, newName) + + await fs.rename(fullPath, rbDestPath) + + successResponse(res, null) + }), +) + +router.post( + '/exists', + validateBody(pathSchema), + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath } = req.body + const { fullPath } = resolveNotebookPath(relPath) + try { + const stats = await fs.stat(fullPath) + const type: PathExistsDTO['type'] = stats.isDirectory() ? 'dir' : stats.isFile() ? 'file' : null + successResponse(res, { exists: true, type }) + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') { + successResponse(res, { exists: false, type: null }) + return + } + throw err + } + }), +) + +router.post( + '/create/dir', + validateBody(createDirSchema), + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath } = req.body + const { fullPath } = resolveNotebookPath(relPath) + + try { + await fs.mkdir(fullPath, { recursive: true }) + } catch (err: unknown) { + if (isNodeError(err)) { + if (err.code === 'EEXIST') { + throw new AlreadyExistsError('路径已存在') + } + if (err.code === 'EACCES') { + throw new ForbiddenError('没有权限创建目录') + } + } + throw err + } + + successResponse(res, null) + }), +) + +router.post( + '/create/file', + validateBody(createFileSchema), + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath } = req.body + const { fullPath } = resolveNotebookPath(relPath) + await fs.mkdir(path.dirname(fullPath), { recursive: true }) + + try { + const fileName = path.basename(relPath, '.md') + const content = `# ${fileName}` + await fs.writeFile(fullPath, content, { encoding: 'utf-8', flag: 'wx' }) + } catch (err: unknown) { + if (isNodeError(err)) { + if (err.code === 'EEXIST') throw new AlreadyExistsError('路径已存在') + if (err.code === 'EACCES') throw new ForbiddenError('没有权限创建文件') + } + throw err + } + + successResponse(res, null) + }), +) + +router.post( + '/rename', + validateBody(renameSchema), + asyncHandler(async (req: Request, res: Response) => { + const { oldPath, newPath } = req.body + const { fullPath: oldFullPath } = resolveNotebookPath(oldPath) + const { fullPath: newFullPath } = resolveNotebookPath(newPath) + + await fs.mkdir(path.dirname(newFullPath), { recursive: true }) + + try { + await fs.rename(oldFullPath, newFullPath) + } catch (err: unknown) { + if (isNodeError(err)) { + if (err.code === 'ENOENT') { + throw new NotFoundError('文件不存在') + } + if (err.code === 'EEXIST') { + throw new AlreadyExistsError('路径已存在') + } + if (err.code === 'EPERM' || err.code === 'EACCES') { + throw new ForbiddenError('没有权限重命名文件或目录') + } + if (err.code === 'EBUSY') { + throw new ResourceLockedError('文件或目录正在使用中或被锁定') + } + } + logger.error('重命名错误:', err) + throw new InternalError('重命名文件或目录失败') + } + + successResponse(res, null) + }), +) + +export default router diff --git a/api/core/search/routes.ts b/api/core/search/routes.ts new file mode 100644 index 0000000..3238fab --- /dev/null +++ b/api/core/search/routes.ts @@ -0,0 +1,97 @@ +import express, { type Request, type Response } from 'express' +import fs from 'fs/promises' +import path from 'path' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import type { FileItemDTO } from '../../../shared/types.js' +import { toPosixPath } from '../../../shared/utils/path.js' + +const router = express.Router() + +router.post( + '/', + asyncHandler(async (req: Request, res: Response) => { + const { keywords } = req.body as { keywords?: string[] } + if (!keywords || !Array.isArray(keywords) || keywords.length === 0) { + successResponse(res, { items: [] }) + return + } + + const searchTerms = keywords.map(k => k.trim().toLowerCase()).filter(k => k.length > 0) + + if (searchTerms.length === 0) { + successResponse(res, { items: [] }) + return + } + + const { fullPath: rootPath } = resolveNotebookPath('') + const results: FileItemDTO[] = [] + const maxResults = 100 + + const searchDir = async (dir: string, relativeDir: string) => { + if (results.length >= maxResults) return + + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + if (results.length >= maxResults) break + + const entryPath = path.join(dir, entry.name) + const entryRelativePath = path.join(relativeDir, entry.name) + + if (entry.name.startsWith('.') || entry.name === 'RB' || entry.name === 'node_modules') continue + + if (entry.isDirectory()) { + await searchDir(entryPath, entryRelativePath) + } else if (entry.isFile()) { + const fileNameLower = entry.name.toLowerCase() + let contentLower = '' + let contentLoaded = false + + const checkKeyword = async (term: string) => { + if (fileNameLower.includes(term)) return true + + if (entry.name.toLowerCase().endsWith('.md')) { + if (!contentLoaded) { + try { + const content = await fs.readFile(entryPath, 'utf-8') + contentLower = content.toLowerCase() + contentLoaded = true + } catch { + return false + } + } + return contentLower.includes(term) + } + return false + } + + let allMatched = true + for (const term of searchTerms) { + const matched = await checkKeyword(term) + if (!matched) { + allMatched = false + break + } + } + + if (allMatched) { + results.push({ + name: entry.name, + path: toPosixPath(entryRelativePath), + type: 'file', + size: 0, + modified: new Date().toISOString(), + }) + } + } + } + } + + await searchDir(rootPath, '') + successResponse(res, { items: results, limited: results.length >= maxResults }) + }), +) + +export default router diff --git a/api/core/settings/routes.ts b/api/core/settings/routes.ts new file mode 100644 index 0000000..d58fd55 --- /dev/null +++ b/api/core/settings/routes.ts @@ -0,0 +1,54 @@ +import express, { type Request, type Response } from 'express' +import fs from 'fs/promises' +import path from 'path' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { NOTEBOOK_ROOT } from '../../config/paths.js' +import type { SettingsDTO } from '../../../shared/types.js' + +const router = express.Router() + +const getSettingsPath = () => path.join(NOTEBOOK_ROOT, '.config', 'settings.json') + +router.get( + '/', + asyncHandler(async (req: Request, res: Response) => { + const settingsPath = getSettingsPath() + try { + const content = await fs.readFile(settingsPath, 'utf-8') + const settings = JSON.parse(content) + successResponse(res, settings) + } catch (error) { + successResponse(res, {}) + } + }), +) + +router.post( + '/', + asyncHandler(async (req: Request, res: Response) => { + const settings = req.body as SettingsDTO + const settingsPath = getSettingsPath() + const configDir = path.dirname(settingsPath) + + try { + await fs.mkdir(configDir, { recursive: true }) + + let existingSettings = {} + try { + const content = await fs.readFile(settingsPath, 'utf-8') + existingSettings = JSON.parse(content) + } catch { + } + + const newSettings = { ...existingSettings, ...settings } + await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2), 'utf-8') + + successResponse(res, newSettings) + } catch (error) { + throw error + } + }), +) + +export default router diff --git a/api/core/upload/routes.ts b/api/core/upload/routes.ts new file mode 100644 index 0000000..76dfd59 --- /dev/null +++ b/api/core/upload/routes.ts @@ -0,0 +1,100 @@ +import express, { type Request, type Response } from 'express' +import fs from 'fs/promises' +import path from 'path' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { getUniqueFilename, mimeToExt, validateImageBuffer, detectImageMimeType } from '../../utils/file.js' +import { NOTEBOOK_ROOT } from '../../config/paths.js' +import { toPosixPath } from '../../../shared/utils/path.js' +import { pad2, formatTimestamp } from '../../../shared/utils/date.js' +import { ValidationError, UnsupportedMediaTypeError } from '../../../shared/errors/index.js' + +const router = express.Router() + +const parseImageDataUrl = (dataUrl: string): { mimeType: string; base64Data: string } | null => { + const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/) + if (!match) return null + const [, mimeType, base64Data] = match + return { mimeType, base64Data: base64Data.replace(/\s/g, '') } +} + +router.post( + '/image', + asyncHandler(async (req: Request, res: Response) => { + const { image } = req.body as { image?: string } + if (!image) throw new ValidationError('需要图片数据') + + const parsed = parseImageDataUrl(image) + if (!parsed) { + throw new ValidationError('无效的图片数据URL') + } + + const ext = mimeToExt[parsed.mimeType] + if (!ext) { + throw new UnsupportedMediaTypeError('不支持的图片类型') + } + + const buffer = Buffer.from(parsed.base64Data, 'base64') + + validateImageBuffer(buffer, parsed.mimeType) + + const detectedMimeType = detectImageMimeType(buffer) + if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { + throw new ValidationError('图片内容类型不匹配或图片已损坏') + } + + const now = new Date() + const year = now.getFullYear() + const month = pad2(now.getMonth() + 1) + const day = pad2(now.getDate()) + + const imagesSubDir = `images/${year}/${month}/${day}` + const { fullPath: imagesDirFullPath } = resolveNotebookPath(imagesSubDir) + await fs.mkdir(imagesDirFullPath, { recursive: true }) + + const baseName = formatTimestamp(now) + const filename = await getUniqueFilename(imagesDirFullPath, baseName, ext) + const relPath = `${imagesSubDir}/${filename}` + const { fullPath } = resolveNotebookPath(relPath) + + await fs.writeFile(fullPath, buffer) + successResponse(res, { name: toPosixPath(relPath), path: toPosixPath(relPath) }) + }), +) + +router.post( + '/wallpaper', + asyncHandler(async (req: Request, res: Response) => { + const { image } = req.body as { image?: string } + if (!image) throw new ValidationError('需要图片数据') + + const parsed = parseImageDataUrl(image) + if (!parsed) { + throw new ValidationError('无效的图片数据URL') + } + + const allowedWallpaperTypes = ['image/png', 'image/jpeg', 'image/webp'] + if (!allowedWallpaperTypes.includes(parsed.mimeType)) { + throw new UnsupportedMediaTypeError('壁纸只支持PNG、JPEG和WebP格式') + } + + const buffer = Buffer.from(parsed.base64Data, 'base64') + + validateImageBuffer(buffer, parsed.mimeType) + + const detectedMimeType = detectImageMimeType(buffer) + if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { + throw new ValidationError('图片内容类型不匹配或图片已损坏') + } + + const configDir = path.join(NOTEBOOK_ROOT, '.config') + const backgroundPath = path.join(configDir, 'background.png') + + await fs.mkdir(configDir, { recursive: true }) + await fs.writeFile(backgroundPath, buffer) + successResponse(res, { message: '壁纸已更新' }) + }), +) + +export default router diff --git a/api/errors/errorCodes.ts b/api/errors/errorCodes.ts new file mode 100644 index 0000000..211626a --- /dev/null +++ b/api/errors/errorCodes.ts @@ -0,0 +1 @@ +export { ERROR_CODES as ErrorCodes, type ErrorCode } from '../../shared/constants/errors.js' diff --git a/api/events/eventBus.ts b/api/events/eventBus.ts new file mode 100644 index 0000000..d2ce2a2 --- /dev/null +++ b/api/events/eventBus.ts @@ -0,0 +1,35 @@ +import type { Response } from 'express' +import { logger } from '../utils/logger.js' + +type NotebookEvent = { + event: string + path?: string +} + +let clients: Response[] = [] + +export const eventBus = { + addClient: (res: Response) => { + clients.push(res) + logger.info(`SSE client connected. Total clients: ${clients.length}`) + }, + removeClient: (res: Response) => { + clients = clients.filter((c) => c !== res) + logger.info(`SSE client disconnected. Total clients: ${clients.length}`) + }, + broadcast: (payload: NotebookEvent) => { + const data = `data: ${JSON.stringify(payload)} + +` + logger.info(`Broadcasting to ${clients.length} clients: ${payload.event} - ${payload.path || ''}`) + clients = clients.filter((client) => { + try { + client.write(data) + return true + } catch (error) { + logger.warn('SSE client write failed, removing') + return false + } + }) + }, +} diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..6531601 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,9 @@ +/** + * Vercel deploy entry handler, for serverless deployment, please don't modify this file + */ +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import app from './app.js'; + +export default function handler(req: VercelRequest, res: VercelResponse) { + return app(req, res); +} \ No newline at end of file diff --git a/api/infra/container.ts b/api/infra/container.ts new file mode 100644 index 0000000..0117981 --- /dev/null +++ b/api/infra/container.ts @@ -0,0 +1,242 @@ +export enum ServiceLifetime { + Singleton = 'singleton', + Transient = 'transient', + Scoped = 'scoped' +} + +export interface ServiceDescriptor { + name: string + factory: () => T | Promise + lifetime: ServiceLifetime + dependencies?: string[] + onDispose?: (instance: T) => void | Promise +} + +export type ServiceFactory = () => T | Promise + +export class ServiceContainer { + private descriptors = new Map() + private singletonInstances = new Map() + private scopedInstances = new Map>() + private resolutionStack: string[] = [] + private disposed = false + + register(name: string, factory: ServiceFactory): void + register(descriptor: ServiceDescriptor): void + register(nameOrDescriptor: string | ServiceDescriptor, factory?: ServiceFactory): void { + this.ensureNotDisposed() + + if (typeof nameOrDescriptor === 'string') { + const descriptor: ServiceDescriptor = { + name: nameOrDescriptor, + factory: factory!, + lifetime: ServiceLifetime.Singleton + } + this.descriptors.set(nameOrDescriptor, descriptor) + } else { + this.descriptors.set(nameOrDescriptor.name, nameOrDescriptor) + } + } + + async get(name: string): Promise { + this.ensureNotDisposed() + return this.resolveInternal(name, null) + } + + getSync(name: string): T { + this.ensureNotDisposed() + + const descriptor = this.descriptors.get(name) + if (!descriptor) { + throw new Error(`Service '${name}' not registered`) + } + + if (descriptor.lifetime === ServiceLifetime.Singleton) { + if (this.singletonInstances.has(name)) { + return this.singletonInstances.get(name) as T + } + } + + const result = descriptor.factory() + if (result instanceof Promise) { + throw new Error( + `Service '${name}' has an async factory but getSync() was called. Use get() instead.` + ) + } + + if (descriptor.lifetime === ServiceLifetime.Singleton) { + this.singletonInstances.set(name, result) + } + + return result as T + } + + createScope(scopeId: string): ServiceScope { + this.ensureNotDisposed() + return new ServiceScope(this, scopeId) + } + + has(name: string): boolean { + return this.descriptors.has(name) + } + + async dispose(): Promise { + if (this.disposed) { + return + } + + for (const [name, instance] of this.singletonInstances) { + const descriptor = this.descriptors.get(name) + if (descriptor?.onDispose) { + try { + await descriptor.onDispose(instance) + } catch (error) { + console.error(`Error disposing service '${name}':`, error) + } + } + } + + for (const [, scopeMap] of this.scopedInstances) { + for (const [name, instance] of scopeMap) { + const descriptor = this.descriptors.get(name) + if (descriptor?.onDispose) { + try { + await descriptor.onDispose(instance) + } catch (error) { + console.error(`Error disposing scoped service '${name}':`, error) + } + } + } + } + + this.singletonInstances.clear() + this.scopedInstances.clear() + this.descriptors.clear() + this.resolutionStack = [] + this.disposed = true + } + + clear(): void { + this.singletonInstances.clear() + this.scopedInstances.clear() + this.descriptors.clear() + this.resolutionStack = [] + } + + isDisposed(): boolean { + return this.disposed + } + + private async resolveInternal(name: string, scopeId: string | null): Promise { + if (this.resolutionStack.includes(name)) { + const cycle = [...this.resolutionStack, name].join(' -> ') + throw new Error(`Circular dependency detected: ${cycle}`) + } + + const descriptor = this.descriptors.get(name) + if (!descriptor) { + throw new Error(`Service '${name}' not registered`) + } + + if (descriptor.lifetime === ServiceLifetime.Singleton) { + if (this.singletonInstances.has(name)) { + return this.singletonInstances.get(name) as T + } + + this.resolutionStack.push(name) + try { + const instance = await descriptor.factory() + this.singletonInstances.set(name, instance) + return instance as T + } finally { + this.resolutionStack.pop() + } + } + + if (descriptor.lifetime === ServiceLifetime.Scoped) { + if (!scopeId) { + throw new Error( + `Scoped service '${name}' cannot be resolved outside of a scope. Use createScope() first.` + ) + } + + let scopeMap = this.scopedInstances.get(scopeId) + if (!scopeMap) { + scopeMap = new Map() + this.scopedInstances.set(scopeId, scopeMap) + } + + if (scopeMap.has(name)) { + return scopeMap.get(name) as T + } + + this.resolutionStack.push(name) + try { + const instance = await descriptor.factory() + scopeMap.set(name, instance) + return instance as T + } finally { + this.resolutionStack.pop() + } + } + + this.resolutionStack.push(name) + try { + const instance = await descriptor.factory() + return instance as T + } finally { + this.resolutionStack.pop() + } + } + + private ensureNotDisposed(): void { + if (this.disposed) { + throw new Error('ServiceContainer has been disposed') + } + } +} + +export class ServiceScope { + private container: ServiceContainer + private scopeId: string + private disposed = false + + constructor(container: ServiceContainer, scopeId: string) { + this.container = container + this.scopeId = scopeId + } + + async get(name: string): Promise { + if (this.disposed) { + throw new Error('ServiceScope has been disposed') + } + return this.container['resolveInternal'](name, this.scopeId) + } + + async dispose(): Promise { + if (this.disposed) { + return + } + + const scopeMap = this.container['scopedInstances'].get(this.scopeId) + if (scopeMap) { + for (const [name, instance] of scopeMap) { + const descriptor = this.container['descriptors'].get(name) + if (descriptor?.onDispose) { + try { + await descriptor.onDispose(instance) + } catch (error) { + console.error(`Error disposing scoped service '${name}':`, error) + } + } + } + this.container['scopedInstances'].delete(this.scopeId) + } + + this.disposed = true + } + + isDisposed(): boolean { + return this.disposed + } +} diff --git a/api/infra/createModule.ts b/api/infra/createModule.ts new file mode 100644 index 0000000..adb5403 --- /dev/null +++ b/api/infra/createModule.ts @@ -0,0 +1,41 @@ +import type { Router } from 'express' +import type { ServiceContainer } from './container.js' +import type { ApiModule, ModuleMetadata, ModuleLifecycle } from './types.js' +import type { ApiModuleConfig, ModuleEndpoints } from '../../shared/modules/types.js' + +export interface CreateModuleOptions { + routes: (container: ServiceContainer) => Router | Promise + lifecycle?: ModuleLifecycle + services?: (container: ServiceContainer) => void | Promise +} + +export function createApiModule< + TId extends string, + TEndpoints extends ModuleEndpoints +>( + config: ApiModuleConfig, + options: CreateModuleOptions +): ApiModule { + const metadata: ModuleMetadata = { + id: config.id, + name: config.name, + version: config.version, + basePath: config.basePath, + order: config.order, + dependencies: config.dependencies, + } + + const lifecycle: ModuleLifecycle | undefined = options.lifecycle + ? options.lifecycle + : options.services + ? { + onLoad: options.services, + } + : undefined + + return { + metadata, + lifecycle, + createRouter: options.routes, + } +} diff --git a/api/infra/index.ts b/api/infra/index.ts new file mode 100644 index 0000000..7634c22 --- /dev/null +++ b/api/infra/index.ts @@ -0,0 +1,5 @@ +export { ServiceContainer } from './container.js' +export type { ServiceFactory } from './container.js' +export { loadModules } from './moduleLoader.js' +export { ModuleManager } from './moduleManager.js' +export type { ApiModule, ModuleMetadata, ModuleLifecycle } from './types.js' diff --git a/api/infra/moduleLoader.ts b/api/infra/moduleLoader.ts new file mode 100644 index 0000000..ac9c121 --- /dev/null +++ b/api/infra/moduleLoader.ts @@ -0,0 +1,24 @@ +import type { Application } from 'express' +import type { ServiceContainer } from './container.js' +import type { ApiModule } from './types.js' +import { ModuleManager } from './moduleManager.js' + +export async function loadModules( + app: Application, + container: ServiceContainer, + modules: ApiModule[] +): Promise { + const manager = new ModuleManager(container) + + for (const module of modules) { + await manager.register(module) + } + + for (const module of manager.getAllModules()) { + await manager.activate(module.metadata.id) + const router = await module.createRouter(container) + app.use('/api' + module.metadata.basePath, router) + } + + return manager +} diff --git a/api/infra/moduleManager.ts b/api/infra/moduleManager.ts new file mode 100644 index 0000000..7fd7e82 --- /dev/null +++ b/api/infra/moduleManager.ts @@ -0,0 +1,76 @@ +import { ApiModule, ModuleMetadata } from './types.js' +import { ServiceContainer } from './container.js' + +export class ModuleManager { + private modules = new Map() + private activeModules = new Set() + private container: ServiceContainer + + constructor(container: ServiceContainer) { + this.container = container + } + + async register(module: ApiModule): Promise { + const { id, dependencies = [] } = module.metadata + + for (const dep of dependencies) { + if (!this.modules.has(dep)) { + throw new Error(`Module '${id}' depends on '${dep}' which is not registered`) + } + } + + this.modules.set(id, module) + + if (module.lifecycle?.onLoad) { + await module.lifecycle.onLoad(this.container) + } + } + + async activate(id: string): Promise { + const module = this.modules.get(id) + if (!module) { + throw new Error(`Module '${id}' not found`) + } + + if (this.activeModules.has(id)) { + return + } + + const { dependencies = [] } = module.metadata + for (const dep of dependencies) { + await this.activate(dep) + } + + if (module.lifecycle?.onActivate) { + await module.lifecycle.onActivate(this.container) + } + + this.activeModules.add(id) + } + + async deactivate(id: string): Promise { + const module = this.modules.get(id) + if (!module) return + + if (!this.activeModules.has(id)) return + + if (module.lifecycle?.onDeactivate) { + await module.lifecycle.onDeactivate(this.container) + } + + this.activeModules.delete(id) + } + + getModule(id: string): ApiModule | undefined { + return this.modules.get(id) + } + + getAllModules(): ApiModule[] { + return Array.from(this.modules.values()) + .sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0)) + } + + getActiveModules(): string[] { + return Array.from(this.activeModules) + } +} diff --git a/api/infra/moduleValidator.ts b/api/infra/moduleValidator.ts new file mode 100644 index 0000000..e5fa035 --- /dev/null +++ b/api/infra/moduleValidator.ts @@ -0,0 +1,113 @@ +import { readdirSync, statSync, existsSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import type { ApiModule } from './types.js' +import type { ModuleDefinition } from '../../shared/modules/types.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +function getSharedModulesPath(): string | null { + const possiblePaths = [ + join(__dirname, '../../shared/modules'), + join(__dirname, '../../../shared/modules'), + join((process as unknown as { resourcesPath?: string })?.resourcesPath || '', 'shared/modules'), + ] + + for (const p of possiblePaths) { + if (existsSync(p)) { + return p + } + } + return null +} + +export interface SharedModuleDefinition { + id: string + name: string + backend?: { + enabled?: boolean + } +} + +function needsBackendImplementation(moduleDef: SharedModuleDefinition): boolean { + return moduleDef.backend?.enabled !== false +} + +async function loadModuleDefinitions(): Promise { + const modules: SharedModuleDefinition[] = [] + const sharedModulesPath = getSharedModulesPath() + + if (!sharedModulesPath) { + return modules + } + + const entries = readdirSync(sharedModulesPath) + + for (const entry of entries) { + const entryPath = join(sharedModulesPath, entry) + const stat = statSync(entryPath) + + if (!stat.isDirectory()) { + continue + } + + try { + const moduleExports = await import(`../../shared/modules/${entry}/index.js`) + + for (const key of Object.keys(moduleExports)) { + if (key.endsWith('_MODULE')) { + const moduleDef = moduleExports[key] as ModuleDefinition + if (moduleDef && moduleDef.id) { + modules.push({ + id: moduleDef.id, + name: moduleDef.name, + backend: moduleDef.backend, + }) + } + } + } + } catch { + // 模块加载失败,跳过 + } + } + + return modules +} + +export async function validateModuleConsistency(apiModules: ApiModule[]): Promise { + const sharedModules = await loadModuleDefinitions() + + if (sharedModules.length === 0) { + console.log('[ModuleValidator] Skipping validation (shared modules not found, likely packaged mode)') + return + } + + const apiModuleIds = new Set(apiModules.map((m) => m.metadata.id)) + const errors: string[] = [] + + for (const sharedModule of sharedModules) { + const needsBackend = needsBackendImplementation(sharedModule) + const hasApiModule = apiModuleIds.has(sharedModule.id) + + if (needsBackend && !hasApiModule) { + errors.push( + `Module '${sharedModule.id}' is defined in shared but not registered in API modules` + ) + } + + if (!needsBackend && hasApiModule) { + errors.push( + `Module '${sharedModule.id}' has backend disabled but is registered in API modules` + ) + } + } + + if (errors.length > 0) { + throw new Error(`Module consistency validation failed:\n - ${errors.join('\n - ')}`) + } + + console.log( + `[ModuleValidator] ✓ Module consistency validated: ${sharedModules.length} shared, ${apiModules.length} API` + ) +} diff --git a/api/infra/types.ts b/api/infra/types.ts new file mode 100644 index 0000000..6346818 --- /dev/null +++ b/api/infra/types.ts @@ -0,0 +1,31 @@ +import type { Router, Application } from 'express' +import type { ServiceContainer } from './container.js' + +export interface ModuleMetadata { + id: string + name: string + version: string + basePath: string + dependencies?: string[] + order?: number +} + +export interface ModuleLifecycle { + onLoad?(container: ServiceContainer): void | Promise + onUnload?(container: ServiceContainer): void | Promise + onActivate?(container: ServiceContainer): void | Promise + onDeactivate?(container: ServiceContainer): void | Promise +} + +export interface ApiModule { + metadata: ModuleMetadata + lifecycle?: ModuleLifecycle + createRouter: (container: ServiceContainer) => Router | Promise +} + +export interface LegacyApiModule { + name: string + basePath: string + init?: (app: Application, container: ServiceContainer) => void | Promise + createRouter: (container: ServiceContainer) => Router | Promise +} diff --git a/api/middlewares/__tests__/errorHandler.test.ts b/api/middlewares/__tests__/errorHandler.test.ts new file mode 100644 index 0000000..b8d997a --- /dev/null +++ b/api/middlewares/__tests__/errorHandler.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { Request, Response, NextFunction } from 'express' + +const { mockIsAppError, mockIsNodeError, mockLoggerError, MockAppError, MockValidationError } = vi.hoisted(() => { + const mockFn = () => ({}) + return { + mockIsAppError: vi.fn(), + mockIsNodeError: vi.fn(), + mockLoggerError: vi.fn(), + MockAppError: class MockAppError extends Error { + statusCode: number + details?: Record + code: string + constructor( + code: string, + message: string, + statusCode: number = 500, + details?: Record + ) { + super(message) + this.name = 'MockAppError' + this.code = code + this.statusCode = statusCode + this.details = details + } + }, + MockValidationError: class MockValidationError extends Error { + statusCode: number + details?: Record + code: string + constructor(message: string, details?: Record) { + super(message) + this.name = 'MockValidationError' + this.code = 'VALIDATION_ERROR' + this.statusCode = 400 + this.details = details + } + }, + } +}) + +vi.mock('@shared/errors', () => ({ + isAppError: mockIsAppError, + isNodeError: mockIsNodeError, + AppError: MockAppError, + ValidationError: MockValidationError, +})) + +vi.mock('@/api/utils/logger', () => ({ + logger: { + error: mockLoggerError, + }, +})) + +import { errorHandler } from '../errorHandler' + +describe('errorHandler', () => { + let mockReq: Request + let mockRes: Response + let mockNext: NextFunction + + beforeEach(() => { + vi.clearAllMocks() + mockReq = {} as Request + mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + mockNext = vi.fn() + mockIsAppError.mockReturnValue(false) + mockIsNodeError.mockReturnValue(false) + }) + + describe('AppError 处理', () => { + it('AppError 应发送自定义状态码和错误码', () => { + const appError = new MockAppError('VALIDATION_ERROR', '验证失败', 400, { field: 'name' }) + mockIsAppError.mockReturnValue(true) + + errorHandler(appError, mockReq, mockRes, mockNext) + + expect(mockRes.status).toHaveBeenCalledWith(400) + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'VALIDATION_ERROR', + message: '验证失败', + details: { field: 'name' }, + }, + }) + }) + + it('ValidationError 应发送正确的错误信息', () => { + const validationError = new MockValidationError('字段不能为空', { field: 'email' }) + mockIsAppError.mockReturnValue(true) + + errorHandler(validationError, mockReq, mockRes, mockNext) + + expect(mockRes.status).toHaveBeenCalledWith(400) + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'VALIDATION_ERROR', + message: '字段不能为空', + details: { field: 'email' }, + }, + }) + }) + + it('AppError 在生产环境不应包含 details', () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + try { + const appError = new MockAppError('VALIDATION_ERROR', '验证失败', 400, { field: 'name' }) + mockIsAppError.mockReturnValue(true) + + errorHandler(appError, mockReq, mockRes, mockNext) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'VALIDATION_ERROR', + message: '验证失败', + details: undefined, + }, + }) + } finally { + process.env.NODE_ENV = originalEnv + } + }) + }) + + describe('Node.js 系统错误处理', () => { + it('Node.js 系统错误应包含 stack 信息(非生产环境)', () => { + const nodeError = new Error('系统错误') as NodeJS.ErrnoException + nodeError.code = 'ENOENT' + nodeError.stack = 'Error: 系统错误\n at Test.' + + mockIsAppError.mockReturnValue(false) + mockIsNodeError.mockReturnValue(true) + + errorHandler(nodeError, mockReq, mockRes, mockNext) + + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'INTERNAL_ERROR', + message: '系统错误', + details: { + stack: nodeError.stack, + nodeErrorCode: 'ENOENT', + }, + }, + }) + }) + + it('普通 Error 应包含 stack 信息(非生产环境)', () => { + const error = new Error('普通错误') + error.stack = 'Error: 普通错误\n at Test.' + + errorHandler(error, mockReq, mockRes, mockNext) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'INTERNAL_ERROR', + message: '普通错误', + details: { + stack: error.stack, + }, + }, + }) + }) + + it('在生产环境不包含敏感信息', () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + try { + const error = new Error('错误信息') + error.stack = 'Error: 错误信息\n at Test.' + + errorHandler(error, mockReq, mockRes, mockNext) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'INTERNAL_ERROR', + message: '错误信息', + details: undefined, + }, + }) + } finally { + process.env.NODE_ENV = originalEnv + } + }) + }) +}) diff --git a/api/middlewares/errorHandler.ts b/api/middlewares/errorHandler.ts new file mode 100644 index 0000000..49ee894 --- /dev/null +++ b/api/middlewares/errorHandler.ts @@ -0,0 +1,42 @@ +import type { NextFunction, Request, Response } from 'express' +import type { ApiResponse } from '../../shared/types.js' +import { ERROR_CODES } from '../../shared/constants/errors.js' +import { isAppError, isNodeError } from '../../shared/errors/index.js' +import { logger } from '../utils/logger.js' + +export const errorHandler = (err: unknown, _req: Request, res: Response, _next: NextFunction) => { + let statusCode: number = 500 + let code: string = ERROR_CODES.INTERNAL_ERROR + let message: string = 'Server internal error' + let details: unknown = undefined + + if (isAppError(err)) { + statusCode = err.statusCode + code = err.code + message = err.message + details = err.details + } else if (isNodeError(err)) { + message = err.message + if (process.env.NODE_ENV !== 'production') { + details = { stack: err.stack, nodeErrorCode: err.code } + } + } else if (err instanceof Error) { + message = err.message + if (process.env.NODE_ENV !== 'production') { + details = { stack: err.stack } + } + } + + logger.error(err) + + const response: ApiResponse = { + success: false, + error: { + code, + message, + details: process.env.NODE_ENV === 'production' ? undefined : details, + }, + } + + res.status(statusCode).json(response) +} diff --git a/api/middlewares/validate.ts b/api/middlewares/validate.ts new file mode 100644 index 0000000..9da0bc2 --- /dev/null +++ b/api/middlewares/validate.ts @@ -0,0 +1,33 @@ +import type { Request, Response, NextFunction } from 'express' +import { ZodSchema, ZodError } from 'zod' +import { ValidationError } from '../../shared/errors/index.js' + +export const validateBody = (schema: ZodSchema) => { + return (req: Request, _res: Response, next: NextFunction) => { + try { + req.body = schema.parse(req.body) + next() + } catch (error) { + if (error instanceof ZodError) { + next(new ValidationError('Request validation failed', { issues: error.issues })) + } else { + next(error) + } + } + } +} + +export const validateQuery = (schema: ZodSchema) => { + return (req: Request, _res: Response, next: NextFunction) => { + try { + req.query = schema.parse(req.query) as typeof req.query + next() + } catch (error) { + if (error instanceof ZodError) { + next(new ValidationError('Query validation failed', { issues: error.issues })) + } else { + next(error) + } + } + } +} diff --git a/api/modules/.gitkeep b/api/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/modules/ai/index.ts b/api/modules/ai/index.ts new file mode 100644 index 0000000..2a54c57 --- /dev/null +++ b/api/modules/ai/index.ts @@ -0,0 +1,17 @@ +import type { Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { AI_MODULE } from '../../../shared/modules/ai/index.js' +import { createAiRoutes } from './routes.js' + +export * from './routes.js' + +export const createAiModule = () => { + return createApiModule(AI_MODULE, { + routes: (_container: ServiceContainer): Router => { + return createAiRoutes() + }, + }) +} + +export default createAiModule diff --git a/api/modules/ai/routes.ts b/api/modules/ai/routes.ts new file mode 100644 index 0000000..83a39db --- /dev/null +++ b/api/modules/ai/routes.ts @@ -0,0 +1,112 @@ +import express, { type Request, type Response } from 'express' +import { spawn } from 'child_process' +import path from 'path' +import { fileURLToPath } from 'url' +import fs from 'fs/promises' +import fsSync from 'fs' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { ValidationError, NotFoundError, InternalError } from '../../../shared/errors/index.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const router = express.Router() + +const PYTHON_TIMEOUT_MS = 30000 + +const spawnPythonWithTimeout = ( + scriptPath: string, + args: string[], + stdinContent: string, + timeoutMs: number = PYTHON_TIMEOUT_MS +): Promise => { + return new Promise((resolve, reject) => { + const pythonProcess = spawn('python', args, { + env: { ...process.env }, + }) + + let stdout = '' + let stderr = '' + let timeoutId: NodeJS.Timeout | null = null + + const cleanup = () => { + if (timeoutId) { + clearTimeout(timeoutId) + timeoutId = null + } + } + + timeoutId = setTimeout(() => { + cleanup() + pythonProcess.kill() + reject(new Error(`Python script timed out after ${timeoutMs}ms`)) + }, timeoutMs) + + pythonProcess.stdout.on('data', (data) => { + stdout += data.toString() + }) + + pythonProcess.stderr.on('data', (data) => { + stderr += data.toString() + }) + + pythonProcess.on('close', (code) => { + cleanup() + if (code !== 0) { + reject(new Error(`Python script exited with code ${code}. Stderr: ${stderr}`)) + } else { + resolve(stdout) + } + }) + + pythonProcess.on('error', (err) => { + cleanup() + reject(new Error(`Failed to start python process: ${err.message}`)) + }) + + pythonProcess.stdin.write(stdinContent) + pythonProcess.stdin.end() + }) +} + +router.post( + '/doubao', + asyncHandler(async (req: Request, res: Response) => { + const { task, path: relPath } = req.body as { task?: string; path?: string } + + if (!task) throw new ValidationError('Task is required') + if (!relPath) throw new ValidationError('Path is required') + + const { fullPath } = resolveNotebookPath(relPath) + + try { + await fs.access(fullPath) + } catch { + throw new NotFoundError('File not found') + } + + const content = await fs.readFile(fullPath, 'utf-8') + + const projectRoot = path.resolve(__dirname, '..', '..', '..') + const scriptPath = path.join(projectRoot, 'tools', 'doubao', 'main.py') + + if (!fsSync.existsSync(scriptPath)) { + throw new InternalError(`Python script not found: ${scriptPath}`) + } + + try { + const result = await spawnPythonWithTimeout(scriptPath, ['--task', task], content) + await fs.writeFile(fullPath, result, 'utf-8') + successResponse(res, { message: 'Task completed successfully' }) + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error' + throw new InternalError(`AI task failed: ${message}`) + } + }) +) + +export const createAiRoutes = (): express.Router => router + +export default createAiRoutes diff --git a/api/modules/document-parser/blogRoutes.ts b/api/modules/document-parser/blogRoutes.ts new file mode 100644 index 0000000..68e0901 --- /dev/null +++ b/api/modules/document-parser/blogRoutes.ts @@ -0,0 +1,217 @@ +import express, { type Request, type Response } from 'express' +import path from 'path' +import fs from 'fs/promises' +import { existsSync } from 'fs' +import axios from 'axios' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { getUniqueFilename } from '../../utils/file.js' +import { formatTimestamp } from '../../../shared/utils/date.js' +import { getTempDir } from '../../utils/tempDir.js' +import { + createJobContext, + spawnPythonScript, + findImageDestinations, + applyReplacements, + copyLocalImage, + cleanupJob, + getScriptPath, + ensureScriptExists, +} from './documentParser.js' +import type { ImageReplacement } from './documentParser.js' +import { ValidationError, InternalError } from '../../../shared/errors/index.js' +import { logger } from '../../utils/logger.js' + +const router = express.Router() + +const tempDir = getTempDir() + +router.post( + '/parse-local', + asyncHandler(async (req: Request, res: Response) => { + const { htmlPath, htmlDir, assetsDirName, assetsFiles, targetPath } = req.body as { + htmlPath?: string + htmlDir?: string + assetsDirName?: string + assetsFiles?: string[] + targetPath?: string + } + + if (!htmlPath || !htmlDir || !targetPath) { + throw new ValidationError('htmlPath, htmlDir and targetPath are required') + } + + let fullTargetPath: string + try { + const resolved = resolveNotebookPath(targetPath) + fullTargetPath = resolved.fullPath + } catch (error) { + throw error + } + + const scriptPath = getScriptPath('blog', 'parse_blog.py') + if (!ensureScriptExists(scriptPath)) { + throw new InternalError('Parser script not found') + } + + const jobContext = await createJobContext('blog') + + let htmlPathInJob = '' + try { + htmlPathInJob = path.join(jobContext.jobDir, 'input.html') + await fs.copyFile(htmlPath, htmlPathInJob) + + if (assetsDirName && assetsFiles && assetsFiles.length > 0) { + const assetsDirPath = path.join(htmlDir, assetsDirName) + for (const relPath of assetsFiles) { + const srcPath = path.join(assetsDirPath, relPath) + if (existsSync(srcPath)) { + const destPath = path.join(jobContext.jobDir, assetsDirName, relPath) + await fs.mkdir(path.dirname(destPath), { recursive: true }) + await fs.copyFile(srcPath, destPath) + } + } + } + } catch (err) { + await cleanupJob(jobContext.jobDir) + throw err + } + + processHtmlInBackground({ + jobDir: jobContext.jobDir, + htmlPath: htmlPathInJob, + targetPath: fullTargetPath, + cwd: path.dirname(scriptPath), + jobContext, + originalHtmlDir: htmlDir, + originalAssetsDirName: assetsDirName, + }).catch(err => { + logger.error('Background HTML processing failed:', err) + fs.writeFile(fullTargetPath, `# 解析失败\n\n> 错误信息: ${err.message}`, 'utf-8').catch(() => { }) + cleanupJob(jobContext.jobDir).catch(() => { }) + }) + + successResponse(res, { + message: 'HTML parsing started in background.', + status: 'processing' + }) + }), +) + +interface ProcessHtmlArgs { + jobDir: string + htmlPath: string + targetPath: string + cwd: string + jobContext: ReturnType extends Promise ? T : never + originalHtmlDir?: string + originalAssetsDirName?: string +} + +async function processHtmlInBackground(args: ProcessHtmlArgs) { + const { jobDir, htmlPath, targetPath, cwd, jobContext, originalHtmlDir, originalAssetsDirName } = args + try { + await spawnPythonScript({ + scriptPath: 'parse_blog.py', + args: [htmlPath], + cwd, + }) + + const parsedPathObj = path.parse(htmlPath) + const markdownPath = path.join(parsedPathObj.dir, `${parsedPathObj.name}.md`) + + if (!existsSync(markdownPath)) { + throw new Error('Markdown result file not found') + } + + let mdContent = await fs.readFile(markdownPath, 'utf-8') + const ctx = await jobContext + + const htmlDir = path.dirname(htmlPath) + const replacements: ImageReplacement[] = [] + + const destinations = findImageDestinations(mdContent) + for (const dest of destinations) { + const originalSrc = dest.url + if (!originalSrc) continue + + if (originalSrc.startsWith('http://') || originalSrc.startsWith('https://')) { + try { + const response = await axios.get(originalSrc, { responseType: 'arraybuffer', timeout: 10000 }) + const contentType = response.headers['content-type'] + let ext = '.jpg' + if (contentType) { + if (contentType.includes('png')) ext = '.png' + else if (contentType.includes('gif')) ext = '.gif' + else if (contentType.includes('webp')) ext = '.webp' + else if (contentType.includes('svg')) ext = '.svg' + else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = '.jpg' + } + const urlExt = path.extname(originalSrc.split('?')[0]) + if (urlExt) ext = urlExt + + const baseName = formatTimestamp(ctx.now) + const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext) + const newPath = path.join(ctx.destImagesDir, newFilename) + await fs.writeFile(newPath, response.data) + replacements.push({ + start: dest.start, + end: dest.end, + original: originalSrc, + replacement: `/${ctx.imagesSubDir}/${newFilename}` + }) + } catch { } + continue + } + + if (originalSrc.startsWith('data:')) continue + + let result = await copyLocalImage( + originalSrc, + jobDir, + htmlDir, + ctx.destImagesDir, + ctx.imagesSubDir, + ctx.now + ) + + if (!result && originalHtmlDir && originalAssetsDirName) { + const srcWithFiles = originalSrc.replace(/^\.\//, '').replace(/^\//, '') + const possiblePaths = [ + path.join(originalHtmlDir, originalAssetsDirName, srcWithFiles), + path.join(originalHtmlDir, originalAssetsDirName, path.basename(srcWithFiles)), + ] + for (const p of possiblePaths) { + if (existsSync(p)) { + const ext = path.extname(p) || '.jpg' + const baseName = formatTimestamp(ctx.now) + const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext) + const newPath = path.join(ctx.destImagesDir, newFilename) + await fs.copyFile(p, newPath) + result = { newLink: `/${ctx.imagesSubDir}/${newFilename}` } + break + } + } + } + + if (result) { + replacements.push({ + start: dest.start, + end: dest.end, + original: originalSrc, + replacement: result.newLink + }) + } + } + + mdContent = applyReplacements(mdContent, replacements) + + await fs.writeFile(targetPath, mdContent, 'utf-8') + await fs.unlink(markdownPath).catch(() => { }) + } finally { + await cleanupJob(jobDir) + } +} + +export default router diff --git a/api/modules/document-parser/documentParser.ts b/api/modules/document-parser/documentParser.ts new file mode 100644 index 0000000..69928f6 --- /dev/null +++ b/api/modules/document-parser/documentParser.ts @@ -0,0 +1,184 @@ +import path from 'path' +import { spawn } from 'child_process' +import fs from 'fs/promises' +import { existsSync, mkdirSync } from 'fs' +import { PROJECT_ROOT, NOTEBOOK_ROOT, TEMP_ROOT } from '../../config/paths.js' +import { getUniqueFilename } from '../../utils/file.js' +import { formatTimestamp, pad2 } from '../../../shared/utils/date.js' +import { logger } from '../../utils/logger.js' + +if (!existsSync(TEMP_ROOT)) { + mkdirSync(TEMP_ROOT, { recursive: true }) +} + +export interface JobContext { + jobDir: string + now: Date + imagesSubDir: string + destImagesDir: string +} + +export const createJobContext = async (prefix: string): Promise => { + const now = new Date() + const jobDir = path.join(TEMP_ROOT, `${prefix}_${formatTimestamp(now)}`) + await fs.mkdir(jobDir, { recursive: true }) + + const year = now.getFullYear() + const month = pad2(now.getMonth() + 1) + const day = pad2(now.getDate()) + const imagesSubDir = `images/${year}/${month}/${day}` + const destImagesDir = path.join(NOTEBOOK_ROOT, imagesSubDir) + await fs.mkdir(destImagesDir, { recursive: true }) + + return { jobDir, now, imagesSubDir, destImagesDir } +} + +export interface SpawnPythonOptions { + scriptPath: string + args: string[] + cwd: string + inputContent?: string +} + +export const spawnPythonScript = async (options: SpawnPythonOptions): Promise => { + const { scriptPath, args, cwd, inputContent } = options + + return new Promise((resolve, reject) => { + const pythonProcess = spawn('python', ['-X', 'utf8', scriptPath, ...args], { + cwd, + env: { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }, + }) + + let stdout = '' + let stderr = '' + + pythonProcess.stdout.on('data', (data) => { + stdout += data.toString() + }) + + pythonProcess.stderr.on('data', (data) => { + stderr += data.toString() + }) + + pythonProcess.on('close', (code) => { + if (code !== 0) { + logger.error('Python script error:', stderr) + reject(new Error(`Process exited with code ${code}. Error: ${stderr}`)) + } else { + resolve(stdout) + } + }) + + pythonProcess.on('error', (err) => { + reject(err) + }) + + if (inputContent !== undefined) { + pythonProcess.stdin.write(inputContent) + pythonProcess.stdin.end() + } + }) +} + +export interface ImageReplacement { + start: number + end: number + original: string + replacement: string +} + +export const findImageDestinations = (md: string): Array<{ url: string; start: number; end: number }> => { + const results: Array<{ url: string; start: number; end: number }> = [] + let i = 0 + while (i < md.length) { + const bang = md.indexOf('![', i) + if (bang === -1) break + const closeBracket = md.indexOf(']', bang + 2) + if (closeBracket === -1) break + if (md[closeBracket + 1] !== '(') { + i = closeBracket + 1 + continue + } + + const urlStart = closeBracket + 2 + let depth = 1 + let j = urlStart + for (; j < md.length; j++) { + const ch = md[j] + if (ch === '(') depth++ + else if (ch === ')') { + depth-- + if (depth === 0) break + } + } + if (depth !== 0) break + results.push({ url: md.slice(urlStart, j), start: urlStart, end: j }) + i = j + 1 + } + return results +} + +export const applyReplacements = (md: string, replacements: ImageReplacement[]): string => { + const sorted = [...replacements].sort((a, b) => b.start - a.start) + let result = md + for (const r of sorted) { + result = `${result.slice(0, r.start)}${r.replacement}${result.slice(r.end)}` + } + return result +} + +export const copyLocalImage = async ( + src: string, + jobDir: string, + htmlDir: string, + destImagesDir: string, + imagesSubDir: string, + now: Date +): Promise<{ newLink: string } | null> => { + const s0 = src.trim().replace(/^<|>$/g, '') + if (!s0) return null + + let decoded = s0 + try { + decoded = decodeURI(s0) + } catch {} + + const s1 = decoded.replace(/\\/g, '/') + const s2 = s1.startsWith('./') ? s1.slice(2) : s1 + const candidates = s2.startsWith('/') + ? [path.join(jobDir, s2.slice(1)), path.join(htmlDir, s2.slice(1))] + : [path.resolve(htmlDir, s2), path.resolve(jobDir, s2)] + + let foundFile: string | null = null + for (const c of candidates) { + if (existsSync(c)) { + foundFile = c + break + } + } + + if (!foundFile) return null + + const ext = path.extname(foundFile) || '.jpg' + const baseName = formatTimestamp(now) + const newFilename = await getUniqueFilename(destImagesDir, baseName, ext) + const newPath = path.join(destImagesDir, newFilename) + await fs.copyFile(foundFile, newPath) + + return { newLink: `/${imagesSubDir}/${newFilename}` } +} + +export const cleanupJob = async (jobDir: string, additionalPaths: string[] = []): Promise => { + await fs.rm(jobDir, { recursive: true, force: true }).catch(() => {}) + for (const p of additionalPaths) { + await fs.unlink(p).catch(() => {}) + } +} + +export const getScriptPath = (toolName: string, scriptName: string): string => { + return path.join(PROJECT_ROOT, 'tools', toolName, scriptName) +} + +export const ensureScriptExists = (scriptPath: string): boolean => { + return existsSync(scriptPath) +} diff --git a/api/modules/document-parser/index.ts b/api/modules/document-parser/index.ts new file mode 100644 index 0000000..656b798 --- /dev/null +++ b/api/modules/document-parser/index.ts @@ -0,0 +1,23 @@ +import express, { type Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { DOCUMENT_PARSER_MODULE } from '../../../shared/modules/document-parser/index.js' +import blogRoutes from './blogRoutes.js' +import mineruRoutes from './mineruRoutes.js' + +export * from './documentParser.js' +export { default as blogRoutes } from './blogRoutes.js' +export { default as mineruRoutes } from './mineruRoutes.js' + +export const createDocumentParserModule = () => { + return createApiModule(DOCUMENT_PARSER_MODULE, { + routes: (_container: ServiceContainer): Router => { + const router = express.Router() + router.use('/blog', blogRoutes) + router.use('/mineru', mineruRoutes) + return router + }, + }) +} + +export default createDocumentParserModule diff --git a/api/modules/document-parser/mineruRoutes.ts b/api/modules/document-parser/mineruRoutes.ts new file mode 100644 index 0000000..e7dab69 --- /dev/null +++ b/api/modules/document-parser/mineruRoutes.ts @@ -0,0 +1,158 @@ +import express, { type Request, type Response } from 'express' +import multer from 'multer' +import path from 'path' +import fs from 'fs/promises' +import { existsSync } from 'fs' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { getUniqueFilename } from '../../utils/file.js' +import { formatTimestamp } from '../../../shared/utils/date.js' +import { getTempDir } from '../../utils/tempDir.js' +import { + createJobContext, + spawnPythonScript, + findImageDestinations, + applyReplacements, + cleanupJob, + getScriptPath, + ensureScriptExists, +} from './documentParser.js' +import type { ImageReplacement } from './documentParser.js' +import { ValidationError, InternalError } from '../../../shared/errors/index.js' +import { logger } from '../../utils/logger.js' + +const router = express.Router() + +const tempDir = getTempDir() + +const upload = multer({ + dest: tempDir, + limits: { + fileSize: 50 * 1024 * 1024 + } +}) + +router.post( + '/parse', + upload.single('file'), + asyncHandler(async (req: Request, res: Response) => { + if (!req.file) { + throw new ValidationError('File is required') + } + + const { targetPath } = req.body as { targetPath?: string } + if (!targetPath) { + await fs.unlink(req.file.path).catch(() => {}) + throw new ValidationError('Target path is required') + } + + let fullTargetPath: string + try { + const resolved = resolveNotebookPath(targetPath) + fullTargetPath = resolved.fullPath + } catch (error) { + await fs.unlink(req.file.path).catch(() => {}) + throw error + } + + const scriptPath = getScriptPath('mineru', 'mineru_parser.py') + if (!ensureScriptExists(scriptPath)) { + await fs.unlink(req.file.path).catch(() => {}) + throw new InternalError('Parser script not found') + } + + processPdfInBackground(req.file.path, fullTargetPath, path.dirname(scriptPath)) + .catch(err => { + logger.error('Background PDF processing failed:', err) + fs.writeFile(fullTargetPath, `# 解析失败\n\n> 错误信息: ${err.message}`, 'utf-8').catch(() => {}) + }) + + successResponse(res, { + message: 'PDF upload successful. Parsing started in background.', + status: 'processing' + }) + }), +) + +async function processPdfInBackground(filePath: string, targetPath: string, cwd: string) { + try { + const output = await spawnPythonScript({ + scriptPath: 'mineru_parser.py', + args: [filePath], + cwd, + }) + + const match = output.match(/JSON_RESULT:(.*)/) + if (!match) { + throw new Error('Failed to parse Python script output: JSON_RESULT not found') + } + + const result = JSON.parse(match[1]) + const markdownPath = result.markdown_file + const outputDir = result.output_dir + + if (!existsSync(markdownPath)) { + throw new Error('Markdown result file not found') + } + + let mdContent = await fs.readFile(markdownPath, 'utf-8') + + const imagesDir = path.join(outputDir, 'images') + if (existsSync(imagesDir)) { + const jobContext = await createJobContext('pdf_images') + + const destinations = findImageDestinations(mdContent) + const replacements: ImageReplacement[] = [] + + for (const dest of destinations) { + const originalSrc = dest.url + if (!originalSrc) continue + + const possibleFilenames = [originalSrc, path.basename(originalSrc)] + let foundFile: string | null = null + + for (const fname of possibleFilenames) { + const localPath = path.join(imagesDir, fname) + if (existsSync(localPath)) { + foundFile = localPath + break + } + + const directPath = path.join(outputDir, originalSrc) + if (existsSync(directPath)) { + foundFile = directPath + break + } + } + + if (foundFile) { + const ext = path.extname(foundFile) + const baseName = formatTimestamp(jobContext.now) + const newFilename = await getUniqueFilename(jobContext.destImagesDir, baseName, ext) + const newPath = path.join(jobContext.destImagesDir, newFilename) + await fs.copyFile(foundFile, newPath) + replacements.push({ + start: dest.start, + end: dest.end, + original: originalSrc, + replacement: `${jobContext.imagesSubDir}/${newFilename}` + }) + } + } + + mdContent = applyReplacements(mdContent, replacements) + } + + await fs.writeFile(targetPath, mdContent, 'utf-8') + await fs.unlink(markdownPath).catch(() => {}) + + if (outputDir && outputDir.includes('temp')) { + await fs.rm(outputDir, { recursive: true, force: true }).catch(() => {}) + } + } finally { + await fs.unlink(filePath).catch(() => {}) + } +} + +export default router diff --git a/api/modules/index.ts b/api/modules/index.ts new file mode 100644 index 0000000..2acb2b2 --- /dev/null +++ b/api/modules/index.ts @@ -0,0 +1,68 @@ +import { readdirSync, statSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import type { ApiModule } from '../infra/types.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const moduleFactoryPattern = /^create\w+Module$/ + +async function discoverModules(): Promise { + const modules: ApiModule[] = [] + const entries = readdirSync(__dirname) + + for (const entry of entries) { + const entryPath = join(__dirname, entry) + + try { + const stats = statSync(entryPath) + if (!stats.isDirectory()) { + continue + } + + const moduleIndexPath = join(entryPath, 'index.ts') + let moduleIndexStats: ReturnType + try { + moduleIndexStats = statSync(moduleIndexPath) + } catch { + continue + } + if (!moduleIndexStats.isFile()) { + continue + } + + const moduleExports = await import(`./${entry}/index.js`) + + for (const exportName of Object.keys(moduleExports)) { + if (moduleFactoryPattern.test(exportName)) { + const factory = moduleExports[exportName] + if (typeof factory === 'function') { + const module = factory() as ApiModule + modules.push(module) + } + } + } + } catch (error) { + console.warn(`[ModuleLoader] Failed to load module '${entry}':`, error) + } + } + + modules.sort((a, b) => { + const orderA = a.metadata.order ?? 0 + const orderB = b.metadata.order ?? 0 + return orderA - orderB + }) + + return modules +} + +export const apiModules: ApiModule[] = await discoverModules() + +export * from './todo/index.js' +export * from './time-tracking/index.js' +export * from './recycle-bin/index.js' +export * from './pydemos/index.js' +export * from './document-parser/index.js' +export * from './ai/index.js' +export * from './remote/index.js' diff --git a/api/modules/pydemos/index.ts b/api/modules/pydemos/index.ts new file mode 100644 index 0000000..0c096c2 --- /dev/null +++ b/api/modules/pydemos/index.ts @@ -0,0 +1,17 @@ +import type { Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { PYDEMOS_MODULE } from '../../../shared/modules/pydemos/index.js' +import { createPyDemosRoutes } from './routes.js' + +export * from './routes.js' + +export const createPyDemosModule = () => { + return createApiModule(PYDEMOS_MODULE, { + routes: (_container: ServiceContainer): Router => { + return createPyDemosRoutes() + }, + }) +} + +export default createPyDemosModule diff --git a/api/modules/pydemos/routes.ts b/api/modules/pydemos/routes.ts new file mode 100644 index 0000000..f5469fc --- /dev/null +++ b/api/modules/pydemos/routes.ts @@ -0,0 +1,258 @@ +import express, { type Request, type Response, type Router } from 'express' +import fs from 'fs/promises' +import path from 'path' +import multer from 'multer' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { getTempDir } from '../../utils/tempDir.js' +import { validateBody, validateQuery } from '../../middlewares/validate.js' +import { + listPyDemosQuerySchema, + createPyDemoSchema, + deletePyDemoSchema, + renamePyDemoSchema, +} from '../../schemas/index.js' +import { NotFoundError, AlreadyExistsError, isNodeError, ValidationError } from '../../../shared/errors/index.js' +import type { PyDemoItem, PyDemoMonth } from '../../../shared/types/pydemos.js' + +const tempDir = getTempDir() + +const upload = multer({ + dest: tempDir, + limits: { + fileSize: 50 * 1024 * 1024 + } +}) + +const toPosixPath = (p: string) => p.replace(/\\/g, '/') + +const getYearPath = (year: number): { relPath: string; fullPath: string } => { + const relPath = `pydemos/${year}` + const { fullPath } = resolveNotebookPath(relPath) + return { relPath, fullPath } +} + +const getMonthPath = (year: number, month: number): { relPath: string; fullPath: string } => { + const monthStr = month.toString().padStart(2, '0') + const relPath = `pydemos/${year}/${monthStr}` + const { fullPath } = resolveNotebookPath(relPath) + return { relPath, fullPath } +} + +const countFilesInDir = async (dirPath: string): Promise => { + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }) + return entries.filter(e => e.isFile()).length + } catch { + return 0 + } +} + +export const createPyDemosRoutes = (): Router => { + const router = express.Router() + + router.get( + '/', + validateQuery(listPyDemosQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const year = parseInt(req.query.year as string) || new Date().getFullYear() + + const { fullPath: yearPath } = getYearPath(year) + const months: PyDemoMonth[] = [] + + try { + await fs.access(yearPath) + } catch { + successResponse(res, { months }) + return + } + + const monthEntries = await fs.readdir(yearPath, { withFileTypes: true }) + + for (const monthEntry of monthEntries) { + if (!monthEntry.isDirectory()) continue + + const monthNum = parseInt(monthEntry.name) + if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) continue + + const monthPath = path.join(yearPath, monthEntry.name) + const demoEntries = await fs.readdir(monthPath, { withFileTypes: true }) + + const demos: PyDemoItem[] = [] + + for (const demoEntry of demoEntries) { + if (!demoEntry.isDirectory()) continue + + const demoPath = path.join(monthPath, demoEntry.name) + const relDemoPath = `pydemos/${year}/${monthEntry.name}/${demoEntry.name}` + + let created: string + try { + const stats = await fs.stat(demoPath) + created = stats.birthtime.toISOString() + } catch { + created = new Date().toISOString() + } + + const fileCount = await countFilesInDir(demoPath) + + demos.push({ + name: demoEntry.name, + path: toPosixPath(relDemoPath), + created, + fileCount + }) + } + + demos.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()) + + if (demos.length > 0) { + months.push({ + month: monthNum, + demos + }) + } + } + + months.sort((a, b) => a.month - b.month) + + successResponse(res, { months }) + }), + ) + + router.post( + '/create', + upload.array('files'), + validateBody(createPyDemoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { name, year, month, folderStructure } = req.body + + const yearNum = parseInt(year) + const monthNum = parseInt(month) + + if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(name)) { + throw new ValidationError('Invalid name format') + } + + const { fullPath: monthPath, relPath: monthRelPath } = getMonthPath(yearNum, monthNum) + const demoPath = path.join(monthPath, name) + const relDemoPath = `${monthRelPath}/${name}` + + try { + await fs.access(demoPath) + throw new AlreadyExistsError('Demo already exists') + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') { + // 目录不存在,可以创建 + } else if (err instanceof AlreadyExistsError) { + throw err + } else { + throw err + } + } + + await fs.mkdir(demoPath, { recursive: true }) + + const files = req.files as Express.Multer.File[] | undefined + let fileCount = 0 + + if (files && files.length > 0) { + let structure: Record = {} + if (folderStructure) { + try { + structure = JSON.parse(folderStructure) + } catch { + structure = {} + } + } + + for (const file of files) { + const relativePath = structure[file.originalname] || file.originalname + const targetPath = path.join(demoPath, relativePath) + const targetDir = path.dirname(targetPath) + + await fs.mkdir(targetDir, { recursive: true }) + await fs.copyFile(file.path, targetPath) + await fs.unlink(file.path).catch(() => { }) + fileCount++ + } + } + + successResponse(res, { path: toPosixPath(relDemoPath), fileCount }) + }), + ) + + router.delete( + '/delete', + validateBody(deletePyDemoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { path: demoPath } = req.body + + if (!demoPath.startsWith('pydemos/')) { + throw new ValidationError('Invalid path') + } + + const { fullPath } = resolveNotebookPath(demoPath) + + try { + await fs.access(fullPath) + } catch { + throw new NotFoundError('Demo not found') + } + + await fs.rm(fullPath, { recursive: true, force: true }) + + successResponse(res, null) + }), + ) + + router.post( + '/rename', + validateBody(renamePyDemoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { oldPath, newName } = req.body + + if (!oldPath.startsWith('pydemos/')) { + throw new ValidationError('Invalid path') + } + + if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(newName)) { + throw new ValidationError('Invalid name format') + } + + const { fullPath: oldFullPath } = resolveNotebookPath(oldPath) + + try { + await fs.access(oldFullPath) + } catch { + throw new NotFoundError('Demo not found') + } + + const parentDir = path.dirname(oldFullPath) + const newFullPath = path.join(parentDir, newName) + const newPath = toPosixPath(path.join(path.dirname(oldPath), newName)) + + try { + await fs.access(newFullPath) + throw new AlreadyExistsError('Demo with this name already exists') + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') { + // 目录不存在,可以重命名 + } else if (err instanceof AlreadyExistsError) { + throw err + } else { + throw err + } + } + + await fs.rename(oldFullPath, newFullPath) + + successResponse(res, { newPath }) + }), + ) + + return router +} + +export default createPyDemosRoutes() diff --git a/api/modules/recycle-bin/index.ts b/api/modules/recycle-bin/index.ts new file mode 100644 index 0000000..a829d85 --- /dev/null +++ b/api/modules/recycle-bin/index.ts @@ -0,0 +1,17 @@ +import type { Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { RECYCLE_BIN_MODULE } from '../../../shared/modules/recycle-bin/index.js' +import router from './routes.js' + +export * from './recycleBinService.js' + +export const createRecycleBinModule = () => { + return createApiModule(RECYCLE_BIN_MODULE, { + routes: (_container: ServiceContainer): Router => { + return router + }, + }) +} + +export default createRecycleBinModule diff --git a/api/modules/recycle-bin/recycleBinService.ts b/api/modules/recycle-bin/recycleBinService.ts new file mode 100644 index 0000000..b0dfc54 --- /dev/null +++ b/api/modules/recycle-bin/recycleBinService.ts @@ -0,0 +1,78 @@ +import fs from 'fs/promises' +import path from 'path' +import { resolveNotebookPath } from '../../utils/pathSafety.js' + +export async function restoreFile( + srcPath: string, + destPath: string, + deletedDate: string, + year: string, + month: string, + day: string +) { + const { fullPath: imagesDir } = resolveNotebookPath(`images/${year}/${month}/${day}`) + + let content = await fs.readFile(srcPath, 'utf-8') + + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g + let match + const imageReplacements: { oldPath: string; newPath: string }[] = [] + + while ((match = imageRegex.exec(content)) !== null) { + const imagePath = match[2] + const imageName = path.basename(imagePath) + + const rbImageName = `${deletedDate}_${imageName}` + const { fullPath: srcImagePath } = resolveNotebookPath(`RB/${rbImageName}`) + + try { + await fs.access(srcImagePath) + await fs.mkdir(imagesDir, { recursive: true }) + const destImagePath = path.join(imagesDir, imageName) + + await fs.rename(srcImagePath, destImagePath) + + const newImagePath = `images/${year}/${month}/${day}/${imageName}` + imageReplacements.push({ oldPath: imagePath, newPath: newImagePath }) + } catch { + } + } + + for (const { oldPath, newPath } of imageReplacements) { + content = content.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath) + } + + await fs.writeFile(destPath, content, 'utf-8') + await fs.unlink(srcPath) +} + +export async function restoreFolder( + srcPath: string, + destPath: string, + deletedDate: string, + year: string, + month: string, + day: string +) { + await fs.mkdir(destPath, { recursive: true }) + + const entries = await fs.readdir(srcPath, { withFileTypes: true }) + + for (const entry of entries) { + const srcEntryPath = path.join(srcPath, entry.name) + const destEntryPath = path.join(destPath, entry.name) + + if (entry.isDirectory()) { + await restoreFolder(srcEntryPath, destEntryPath, deletedDate, year, month, day) + } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { + await restoreFile(srcEntryPath, destEntryPath, deletedDate, year, month, day) + } else { + await fs.rename(srcEntryPath, destEntryPath) + } + } + + const remaining = await fs.readdir(srcPath) + if (remaining.length === 0) { + await fs.rmdir(srcPath) + } +} diff --git a/api/modules/recycle-bin/routes.ts b/api/modules/recycle-bin/routes.ts new file mode 100644 index 0000000..74a55fd --- /dev/null +++ b/api/modules/recycle-bin/routes.ts @@ -0,0 +1,175 @@ +import express, { type Request, type Response } from 'express' +import fs from 'fs/promises' +import path from 'path' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { restoreFile, restoreFolder } from './recycleBinService.js' +import { + NotFoundError, + BadRequestError, + ValidationError, + AlreadyExistsError, +} from '../../../shared/errors/index.js' + +const router = express.Router() + +router.get( + '/', + asyncHandler(async (req: Request, res: Response) => { + const { fullPath: rbDir } = resolveNotebookPath('RB') + + try { + await fs.access(rbDir) + } catch { + successResponse(res, { groups: [] }) + return + } + + const entries = await fs.readdir(rbDir, { withFileTypes: true }) + + const items: { name: string; originalName: string; type: 'file' | 'dir'; deletedDate: string; path: string }[] = [] + + for (const entry of entries) { + const match = entry.name.match(/^(\d{8})_(.+)$/) + if (!match) continue + + const [, dateStr, originalName] = match + + if (entry.isDirectory()) { + items.push({ + name: entry.name, + originalName, + type: 'dir', + deletedDate: dateStr, + path: `RB/${entry.name}`, + }) + } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { + items.push({ + name: entry.name, + originalName, + type: 'file', + deletedDate: dateStr, + path: `RB/${entry.name}`, + }) + } + } + + const groupedMap = new Map() + for (const item of items) { + const existing = groupedMap.get(item.deletedDate) || [] + existing.push(item) + groupedMap.set(item.deletedDate, existing) + } + + const groups = Array.from(groupedMap.entries()) + .map(([date, items]) => ({ + date, + items: items.sort((a, b) => a.originalName.localeCompare(b.originalName)), + })) + .sort((a, b) => b.date.localeCompare(a.date)) + + successResponse(res, { groups }) + }), +) + +router.post( + '/restore', + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath, type } = req.body as { path?: string; type?: 'file' | 'dir' } + if (!relPath || !type) { + throw new ValidationError('Path and type are required') + } + + const { fullPath: itemPath } = resolveNotebookPath(relPath) + + try { + await fs.access(itemPath) + } catch { + throw new NotFoundError('Item not found in recycle bin') + } + + const match = path.basename(itemPath).match(/^(\d{8})_(.+)$/) + if (!match) { + throw new BadRequestError('Invalid recycle bin item name') + } + + const [, dateStr, originalName] = match + const year = dateStr.substring(0, 4) + const month = dateStr.substring(4, 6) + const day = dateStr.substring(6, 8) + + const { fullPath: markdownsDir } = resolveNotebookPath('markdowns') + await fs.mkdir(markdownsDir, { recursive: true }) + + const destPath = path.join(markdownsDir, originalName) + + const existing = await fs.stat(destPath).catch(() => null) + if (existing) { + throw new AlreadyExistsError('A file or folder with this name already exists') + } + + if (type === 'dir') { + await restoreFolder(itemPath, destPath, dateStr, year, month, day) + } else { + await restoreFile(itemPath, destPath, dateStr, year, month, day) + } + + successResponse(res, null) + }), +) + +router.delete( + '/permanent', + asyncHandler(async (req: Request, res: Response) => { + const { path: relPath, type } = req.body as { path?: string; type?: 'file' | 'dir' } + if (!relPath || !type) { + throw new ValidationError('Path and type are required') + } + + const { fullPath: itemPath } = resolveNotebookPath(relPath) + + try { + await fs.access(itemPath) + } catch { + throw new NotFoundError('Item not found in recycle bin') + } + + if (type === 'dir') { + await fs.rm(itemPath, { recursive: true, force: true }) + } else { + await fs.unlink(itemPath) + } + + successResponse(res, null) + }), +) + +router.delete( + '/empty', + asyncHandler(async (req: Request, res: Response) => { + const { fullPath: rbDir } = resolveNotebookPath('RB') + + try { + await fs.access(rbDir) + } catch { + successResponse(res, null) + return + } + + const entries = await fs.readdir(rbDir, { withFileTypes: true }) + + for (const entry of entries) { + const entryPath = path.join(rbDir, entry.name) + if (entry.isDirectory()) { + await fs.rm(entryPath, { recursive: true, force: true }) + } else { + await fs.unlink(entryPath) + } + } + + successResponse(res, null) + }), +) + +export default router diff --git a/api/modules/remote/index.ts b/api/modules/remote/index.ts new file mode 100644 index 0000000..147b8d0 --- /dev/null +++ b/api/modules/remote/index.ts @@ -0,0 +1,25 @@ +import type { Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { REMOTE_MODULE } from '../../../shared/modules/remote/index.js' +import { RemoteService } from './service.js' +import { createRemoteRoutes } from './routes.js' + +export * from './service.js' +export * from './routes.js' + +export const createRemoteModule = () => { + return createApiModule(REMOTE_MODULE, { + routes: (container: ServiceContainer): Router => { + const remoteService = container.getSync('remoteService') + return createRemoteRoutes({ remoteService }) + }, + lifecycle: { + onLoad: (container: ServiceContainer): void => { + container.register('remoteService', () => new RemoteService()) + }, + }, + }) +} + +export default createRemoteModule diff --git a/api/modules/remote/routes.ts b/api/modules/remote/routes.ts new file mode 100644 index 0000000..b9ac02e --- /dev/null +++ b/api/modules/remote/routes.ts @@ -0,0 +1,80 @@ +import express, { type Request, type Response, type Router } from 'express' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { RemoteService, type DeviceData } from './service.js' + +export interface RemoteRoutesDependencies { + remoteService: RemoteService +} + +export const createRemoteRoutes = (deps: RemoteRoutesDependencies): Router => { + const router = express.Router() + const { remoteService } = deps + + router.get( + '/config', + asyncHandler(async (req: Request, res: Response) => { + const config = await remoteService.getConfig() + successResponse(res, config) + }), + ) + + router.post( + '/config', + asyncHandler(async (req: Request, res: Response) => { + const config = req.body + await remoteService.saveConfig(config) + successResponse(res, null) + }), + ) + + router.get( + '/screenshot', + asyncHandler(async (req: Request, res: Response) => { + const deviceName = req.query.device as string | undefined + const buffer = await remoteService.getScreenshot(deviceName) + if (!buffer) { + return successResponse(res, '') + } + const base64 = `data:image/png;base64,${buffer.toString('base64')}` + successResponse(res, base64) + }), + ) + + router.post( + '/screenshot', + asyncHandler(async (req: Request, res: Response) => { + const { dataUrl, deviceName } = req.body + console.log('[Remote] saveScreenshot called:', { deviceName, hasDataUrl: !!dataUrl }) + await remoteService.saveScreenshot(dataUrl, deviceName) + successResponse(res, null) + }), + ) + + router.get( + '/data', + asyncHandler(async (req: Request, res: Response) => { + const deviceName = req.query.device as string | undefined + const data = await remoteService.getData(deviceName) + successResponse(res, data) + }), + ) + + router.post( + '/data', + asyncHandler(async (req: Request, res: Response) => { + const { deviceName, lastConnected } = req.body + const data: DeviceData = {} + if (lastConnected !== undefined) { + data.lastConnected = lastConnected + } + await remoteService.saveData(data, deviceName) + successResponse(res, null) + }), + ) + + return router +} + +const remoteService = new RemoteService() +export default createRemoteRoutes({ remoteService }) diff --git a/api/modules/remote/service.ts b/api/modules/remote/service.ts new file mode 100644 index 0000000..6251c0b --- /dev/null +++ b/api/modules/remote/service.ts @@ -0,0 +1,178 @@ +import fs from 'fs/promises' +import path from 'path' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import type { RemoteConfig, RemoteDevice } from '../../../shared/modules/remote/types.js' + +export interface RemoteServiceDependencies { } + +const REMOTE_DIR = 'remote' + +export interface DeviceData { + lastConnected?: string +} + +export class RemoteService { + constructor(private deps: RemoteServiceDependencies = {}) { } + + private getRemoteDir(): { relPath: string; fullPath: string } { + const { fullPath } = resolveNotebookPath(REMOTE_DIR) + return { relPath: REMOTE_DIR, fullPath } + } + + private getDeviceDir(deviceName: string): { relPath: string; fullPath: string } { + const safeName = this.sanitizeFileName(deviceName) + const { fullPath } = resolveNotebookPath(path.join(REMOTE_DIR, safeName)) + return { relPath: path.join(REMOTE_DIR, safeName), fullPath } + } + + private sanitizeFileName(name: string): string { + return name.replace(/[<>:"/\\|?*]/g, '_').trim() || 'unnamed' + } + + private getDeviceConfigPath(deviceName: string): string { + const { fullPath } = this.getDeviceDir(deviceName) + return path.join(fullPath, 'config.json') + } + + private getDeviceScreenshotPath(deviceName: string): string { + const { fullPath } = this.getDeviceDir(deviceName) + return path.join(fullPath, 'screenshot.png') + } + + private getDeviceDataPath(deviceName: string): string { + const { fullPath } = this.getDeviceDir(deviceName) + return path.join(fullPath, 'data.json') + } + + private async ensureDir(dirPath: string): Promise { + await fs.mkdir(dirPath, { recursive: true }) + } + + private async getDeviceNames(): Promise { + const { fullPath } = this.getRemoteDir() + try { + const entries = await fs.readdir(fullPath, { withFileTypes: true }) + const dirs = entries.filter(e => e.isDirectory()).map(e => e.name) + return dirs + } catch { + return [] + } + } + + async getConfig(): Promise { + const deviceNames = await this.getDeviceNames() + const devices: RemoteDevice[] = await Promise.all( + deviceNames.map(async (name) => { + try { + const configPath = this.getDeviceConfigPath(name) + const content = await fs.readFile(configPath, 'utf-8') + const deviceConfig = JSON.parse(content) + return { + id: deviceConfig.id || name, + deviceName: name, + serverHost: deviceConfig.serverHost || '', + desktopPort: deviceConfig.desktopPort || 3000, + gitPort: deviceConfig.gitPort || 3001, + } + } catch { + return { + id: name, + deviceName: name, + serverHost: '', + desktopPort: 3000, + gitPort: 3001, + } + } + }) + ) + return { devices } + } + + async saveConfig(config: RemoteConfig): Promise { + const { fullPath: remoteDirFullPath } = this.getRemoteDir() + await this.ensureDir(remoteDirFullPath) + + const existingDevices = await this.getDeviceNames() + const newDeviceNames = config.devices.map(d => this.sanitizeFileName(d.deviceName)) + + for (const oldDevice of existingDevices) { + if (!newDeviceNames.includes(oldDevice)) { + try { + const oldDir = path.join(remoteDirFullPath, oldDevice) + await fs.rm(oldDir, { recursive: true, force: true }) + } catch { } + } + } + + for (const device of config.devices) { + const deviceDir = this.getDeviceDir(device.deviceName) + await this.ensureDir(deviceDir.fullPath) + + const deviceConfigPath = this.getDeviceConfigPath(device.deviceName) + const deviceConfig = { + id: device.id, + serverHost: device.serverHost, + desktopPort: device.desktopPort, + gitPort: device.gitPort, + } + await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), 'utf-8') + } + } + + async getScreenshot(deviceName?: string): Promise { + if (!deviceName) { + return null + } + const screenshotPath = this.getDeviceScreenshotPath(deviceName) + try { + return await fs.readFile(screenshotPath) + } catch { + return null + } + } + + async saveScreenshot(dataUrl: string, deviceName?: string): Promise { + console.log('[RemoteService] saveScreenshot:', { deviceName, dataUrlLength: dataUrl?.length }) + if (!deviceName || deviceName.trim() === '') { + console.warn('[RemoteService] saveScreenshot skipped: no deviceName') + return + } + const deviceDir = this.getDeviceDir(deviceName) + await this.ensureDir(deviceDir.fullPath) + + const base64Data = dataUrl.replace(/^data:image\/png;base64,/, '') + const buffer = Buffer.from(base64Data, 'base64') + + const screenshotPath = this.getDeviceScreenshotPath(deviceName) + await fs.writeFile(screenshotPath, buffer) + } + + async getData(deviceName?: string): Promise { + if (!deviceName || deviceName.trim() === '') { + return null + } + const dataPath = this.getDeviceDataPath(deviceName) + try { + const content = await fs.readFile(dataPath, 'utf-8') + return JSON.parse(content) + } catch { + return null + } + } + + async saveData(data: DeviceData, deviceName?: string): Promise { + if (!deviceName || deviceName.trim() === '') { + console.warn('[RemoteService] saveData skipped: no deviceName') + return + } + const deviceDir = this.getDeviceDir(deviceName) + await this.ensureDir(deviceDir.fullPath) + + const dataPath = this.getDeviceDataPath(deviceName) + await fs.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8') + } +} + +export const createRemoteService = (deps?: RemoteServiceDependencies): RemoteService => { + return new RemoteService(deps) +} diff --git a/api/modules/time-tracking/heartbeatService.ts b/api/modules/time-tracking/heartbeatService.ts new file mode 100644 index 0000000..b93ab3e --- /dev/null +++ b/api/modules/time-tracking/heartbeatService.ts @@ -0,0 +1,80 @@ +import { logger } from '../../utils/logger.js' + +export interface HeartbeatCallback { + (): Promise +} + +export interface HeartbeatState { + lastHeartbeat: Date + isRunning: boolean +} + +const DEFAULT_HEARTBEAT_INTERVAL = 60000 + +export class HeartbeatService { + private interval: NodeJS.Timeout | null = null + private lastHeartbeat: Date = new Date() + private readonly intervalMs: number + private callback: HeartbeatCallback | null = null + + constructor(intervalMs: number = DEFAULT_HEARTBEAT_INTERVAL) { + this.intervalMs = intervalMs + } + + setCallback(callback: HeartbeatCallback): void { + this.callback = callback + } + + start(): void { + if (this.interval) { + this.stop() + } + + this.interval = setInterval(async () => { + if (this.callback) { + try { + this.lastHeartbeat = new Date() + await this.callback() + } catch (err) { + logger.error('Heartbeat callback failed:', err) + } + } + }, this.intervalMs) + + this.lastHeartbeat = new Date() + } + + stop(): void { + if (this.interval) { + clearInterval(this.interval) + this.interval = null + } + } + + isRunning(): boolean { + return this.interval !== null + } + + getLastHeartbeat(): Date { + return this.lastHeartbeat + } + + updateHeartbeat(): void { + this.lastHeartbeat = new Date() + } + + getState(): HeartbeatState { + return { + lastHeartbeat: this.lastHeartbeat, + isRunning: this.isRunning() + } + } + + restoreState(state: { lastHeartbeat: string }): void { + this.lastHeartbeat = new Date(state.lastHeartbeat) + } +} + +export const createHeartbeatService = (intervalMs?: number): HeartbeatService => { + return new HeartbeatService(intervalMs) +} diff --git a/api/modules/time-tracking/index.ts b/api/modules/time-tracking/index.ts new file mode 100644 index 0000000..2454d14 --- /dev/null +++ b/api/modules/time-tracking/index.ts @@ -0,0 +1,38 @@ +import type { Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { TIME_TRACKING_MODULE } from '../../../shared/modules/time-tracking/index.js' +import { + TimeTrackerService, + initializeTimeTrackerService, + type TimeTrackerServiceConfig +} from './timeService.js' +import { createTimeTrackingRoutes } from './routes.js' + +export * from './timeService.js' +export * from './heartbeatService.js' +export * from './sessionPersistence.js' +export * from './routes.js' + +export interface TimeTrackingModuleConfig { + config?: TimeTrackerServiceConfig +} + +export const createTimeTrackingModule = (moduleConfig: TimeTrackingModuleConfig = {}) => { + let serviceInstance: TimeTrackerService | undefined + + return createApiModule(TIME_TRACKING_MODULE, { + routes: (container: ServiceContainer): Router => { + const timeTrackerService = container.getSync('timeTrackerService') + return createTimeTrackingRoutes({ timeTrackerService }) + }, + lifecycle: { + onLoad: async (container: ServiceContainer): Promise => { + serviceInstance = await initializeTimeTrackerService(moduleConfig.config) + container.register('timeTrackerService', () => serviceInstance!) + }, + }, + }) +} + +export default createTimeTrackingModule diff --git a/api/modules/time-tracking/routes.ts b/api/modules/time-tracking/routes.ts new file mode 100644 index 0000000..b41a29e --- /dev/null +++ b/api/modules/time-tracking/routes.ts @@ -0,0 +1,131 @@ +import express, { type Request, type Response, type Router } from 'express' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { TimeTrackerService } from './timeService.js' +import type { TimeTrackingEvent } from '../../../shared/types.js' + +export interface TimeTrackingRoutesDependencies { + timeTrackerService: TimeTrackerService +} + +export const createTimeTrackingRoutes = (deps: TimeTrackingRoutesDependencies): Router => { + const router = express.Router() + const { timeTrackerService } = deps + + router.get( + '/current', + asyncHandler(async (_req: Request, res: Response) => { + const state = timeTrackerService.getCurrentState() + + successResponse(res, { + isRunning: state.isRunning, + isPaused: state.isPaused, + currentSession: state.currentSession ? { + id: state.currentSession.id, + startTime: state.currentSession.startTime, + duration: state.currentSession.duration, + currentTab: state.currentTabRecord ? { + tabId: state.currentTabRecord.tabId, + fileName: state.currentTabRecord.fileName, + tabType: state.currentTabRecord.tabType + } : null + } : null, + todayDuration: state.todayDuration + }) + }) + ) + + router.post( + '/event', + asyncHandler(async (req: Request, res: Response) => { + const event = req.body as TimeTrackingEvent + await timeTrackerService.handleEvent(event) + successResponse(res, null) + }) + ) + + router.get( + '/day/:date', + asyncHandler(async (req: Request, res: Response) => { + const { date } = req.params + const [year, month, day] = date.split('-').map(Number) + const data = await timeTrackerService.getDayData(year, month, day) + + const sessionsCount = data.sessions.length + const averageSessionDuration = sessionsCount > 0 + ? Math.floor(data.totalDuration / sessionsCount) + : 0 + const longestSession = data.sessions.reduce((max, s) => + s.duration > max ? s.duration : max, 0) + + const topTabs = Object.entries(data.tabSummary) + .map(([_, summary]) => ({ + fileName: summary.fileName, + duration: summary.totalDuration + })) + .sort((a, b) => b.duration - a.duration) + .slice(0, 5) + + successResponse(res, { + ...data, + stats: { + sessionsCount, + averageSessionDuration, + longestSession, + topTabs + } + }) + }) + ) + + router.get( + '/week/:startDate', + asyncHandler(async (req: Request, res: Response) => { + const { startDate } = req.params + const [year, month, day] = startDate.split('-').map(Number) + const start = new Date(year, month - 1, day) + const data = await timeTrackerService.getWeekData(start) + + const totalDuration = data.reduce((sum, d) => sum + d.totalDuration, 0) + const activeDays = data.filter(d => d.totalDuration > 0).length + + successResponse(res, { + days: data, + totalDuration, + activeDays, + averageDaily: activeDays > 0 ? Math.floor(totalDuration / activeDays) : 0 + }) + }) + ) + + router.get( + '/month/:yearMonth', + asyncHandler(async (req: Request, res: Response) => { + const { yearMonth } = req.params + const [year, month] = yearMonth.split('-').map(Number) + const data = await timeTrackerService.getMonthData(year, month) + successResponse(res, data) + }) + ) + + router.get( + '/year/:year', + asyncHandler(async (req: Request, res: Response) => { + const { year } = req.params + const data = await timeTrackerService.getYearData(parseInt(year)) + successResponse(res, data) + }) + ) + + router.get( + '/stats', + asyncHandler(async (req: Request, res: Response) => { + const year = req.query.year ? parseInt(req.query.year as string) : undefined + const month = req.query.month ? parseInt(req.query.month as string) : undefined + const stats = await timeTrackerService.getStats(year, month) + successResponse(res, stats) + }) + ) + + return router +} diff --git a/api/modules/time-tracking/sessionPersistence.ts b/api/modules/time-tracking/sessionPersistence.ts new file mode 100644 index 0000000..562cddd --- /dev/null +++ b/api/modules/time-tracking/sessionPersistence.ts @@ -0,0 +1,367 @@ +import fs from 'fs/promises' +import path from 'path' +import { NOTEBOOK_ROOT } from '../../config/paths.js' +import type { + TimingSession, + TabRecord, + DayTimeData, + MonthTimeData, + YearTimeData +} from '../../../shared/types.js' +import { logger } from '../../utils/logger.js' + +const TIME_ROOT = path.join(NOTEBOOK_ROOT, 'time') + +export interface PersistedSessionState { + session: TimingSession | null + currentTabRecord: TabRecord | null + isPaused: boolean + lastHeartbeat: string +} + +export interface SessionPersistence { + loadCurrentState(): Promise + saveCurrentState(state: PersistedSessionState): Promise + clearCurrentState(): Promise + saveSessionToDay(session: TimingSession): Promise + getDayData(year: number, month: number, day: number): Promise + getMonthData(year: number, month: number): Promise + getYearData(year: number): Promise + updateDayDataRealtime( + year: number, + month: number, + day: number, + session: TimingSession, + currentTabRecord: TabRecord | null + ): Promise + updateMonthSummary(year: number, month: number, day: number, duration: number): Promise + updateYearSummary(year: number, month: number, duration: number): Promise + recalculateMonthSummary(year: number, month: number, todayDuration: number): Promise + recalculateYearSummary(year: number): Promise +} + +const getDayFilePath = (year: number, month: number, day: number): string => { + const monthStr = month.toString().padStart(2, '0') + const dayStr = day.toString().padStart(2, '0') + return path.join(TIME_ROOT, year.toString(), `${year}${monthStr}${dayStr}.json`) +} + +const getMonthFilePath = (year: number, month: number): string => { + const monthStr = month.toString().padStart(2, '0') + return path.join(TIME_ROOT, year.toString(), `${year}${monthStr}.json`) +} + +const getYearFilePath = (year: number): string => { + return path.join(TIME_ROOT, 'summary', `${year}.json`) +} + +const ensureDirExists = async (filePath: string): Promise => { + const dir = path.dirname(filePath) + await fs.mkdir(dir, { recursive: true }) +} + +const createEmptyDayData = (year: number, month: number, day: number): DayTimeData => ({ + date: `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`, + totalDuration: 0, + sessions: [], + tabSummary: {}, + lastUpdated: new Date().toISOString() +}) + +const createEmptyMonthData = (year: number, month: number): MonthTimeData => ({ + year, + month, + days: {}, + monthlyTotal: 0, + averageDaily: 0, + activeDays: 0, + lastUpdated: new Date().toISOString() +}) + +const createEmptyYearData = (year: number): YearTimeData => ({ + year, + months: {}, + yearlyTotal: 0, + averageMonthly: 0, + averageDaily: 0, + totalActiveDays: 0 +}) + +class SessionPersistenceService implements SessionPersistence { + private readonly stateFilePath: string + + constructor() { + this.stateFilePath = path.join(TIME_ROOT, '.current-session.json') + } + + async loadCurrentState(): Promise { + try { + const content = await fs.readFile(this.stateFilePath, 'utf-8') + const state = JSON.parse(content) + return { + session: state.session || null, + currentTabRecord: state.currentTabRecord || null, + isPaused: state.isPaused || false, + lastHeartbeat: state.lastHeartbeat || new Date().toISOString() + } + } catch (err) { + logger.debug('No existing session to load or session file corrupted') + return { + session: null, + currentTabRecord: null, + isPaused: false, + lastHeartbeat: new Date().toISOString() + } + } + } + + async saveCurrentState(state: PersistedSessionState): Promise { + await ensureDirExists(this.stateFilePath) + await fs.writeFile(this.stateFilePath, JSON.stringify({ + session: state.session, + currentTabRecord: state.currentTabRecord, + isPaused: state.isPaused, + lastHeartbeat: state.lastHeartbeat + }), 'utf-8') + } + + async clearCurrentState(): Promise { + try { + await fs.unlink(this.stateFilePath) + } catch (err) { + logger.debug('Session state file already removed or does not exist') + } + } + + async saveSessionToDay(session: TimingSession): Promise { + const startTime = new Date(session.startTime) + const year = startTime.getFullYear() + const month = startTime.getMonth() + 1 + const day = startTime.getDate() + + const filePath = getDayFilePath(year, month, day) + await ensureDirExists(filePath) + + let dayData = await this.getDayData(year, month, day) + + dayData.sessions.push(session) + dayData.totalDuration += session.duration + + for (const record of session.tabRecords) { + const key = record.filePath || record.fileName + if (!dayData.tabSummary[key]) { + dayData.tabSummary[key] = { + fileName: record.fileName, + tabType: record.tabType, + totalDuration: 0, + focusCount: 0 + } + } + dayData.tabSummary[key].totalDuration += record.duration + dayData.tabSummary[key].focusCount += record.focusedPeriods.length + } + + dayData.lastUpdated = new Date().toISOString() + await fs.writeFile(filePath, JSON.stringify(dayData, null, 2), 'utf-8') + + await this.updateMonthSummary(year, month, day, session.duration) + await this.updateYearSummary(year, month, session.duration) + } + + async getDayData(year: number, month: number, day: number): Promise { + const filePath = getDayFilePath(year, month, day) + try { + const content = await fs.readFile(filePath, 'utf-8') + return JSON.parse(content) + } catch (err) { + return createEmptyDayData(year, month, day) + } + } + + async getMonthData(year: number, month: number): Promise { + const filePath = getMonthFilePath(year, month) + try { + const content = await fs.readFile(filePath, 'utf-8') + return JSON.parse(content) + } catch (err) { + return createEmptyMonthData(year, month) + } + } + + async getYearData(year: number): Promise { + const filePath = getYearFilePath(year) + try { + const content = await fs.readFile(filePath, 'utf-8') + return JSON.parse(content) + } catch (err) { + return createEmptyYearData(year) + } + } + + async updateDayDataRealtime( + year: number, + month: number, + day: number, + session: TimingSession, + currentTabRecord: TabRecord | null + ): Promise { + const filePath = getDayFilePath(year, month, day) + await ensureDirExists(filePath) + + let dayData = await this.getDayData(year, month, day) + + const currentSessionDuration = session.tabRecords.reduce((sum, r) => sum + r.duration, 0) + + (currentTabRecord?.duration || 0) + + const existingSessionIndex = dayData.sessions.findIndex(s => s.id === session.id) + + const realtimeSession: TimingSession = { + ...session, + duration: currentSessionDuration, + tabRecords: currentTabRecord + ? [...session.tabRecords, currentTabRecord] + : session.tabRecords + } + + if (existingSessionIndex >= 0) { + const oldDuration = dayData.sessions[existingSessionIndex].duration + dayData.sessions[existingSessionIndex] = realtimeSession + dayData.totalDuration += currentSessionDuration - oldDuration + } else { + dayData.sessions.push(realtimeSession) + dayData.totalDuration += currentSessionDuration + } + + dayData.tabSummary = {} + for (const s of dayData.sessions) { + for (const record of s.tabRecords) { + const key = record.filePath || record.fileName + if (!dayData.tabSummary[key]) { + dayData.tabSummary[key] = { + fileName: record.fileName, + tabType: record.tabType, + totalDuration: 0, + focusCount: 0 + } + } + dayData.tabSummary[key].totalDuration += record.duration + dayData.tabSummary[key].focusCount += record.focusedPeriods.length + } + } + + dayData.lastUpdated = new Date().toISOString() + await fs.writeFile(filePath, JSON.stringify(dayData, null, 2), 'utf-8') + + return dayData + } + + async updateMonthSummary(year: number, month: number, day: number, duration: number): Promise { + const filePath = getMonthFilePath(year, month) + await ensureDirExists(filePath) + + let monthData = await this.getMonthData(year, month) + + const dayStr = day.toString().padStart(2, '0') + if (!monthData.days[dayStr]) { + monthData.days[dayStr] = { totalDuration: 0, sessions: 0, topTabs: [] } + } + + monthData.days[dayStr].totalDuration += duration + monthData.days[dayStr].sessions += 1 + monthData.monthlyTotal += duration + monthData.activeDays = Object.keys(monthData.days).length + monthData.averageDaily = Math.floor(monthData.monthlyTotal / monthData.activeDays) + monthData.lastUpdated = new Date().toISOString() + + await fs.writeFile(filePath, JSON.stringify(monthData, null, 2), 'utf-8') + } + + async updateYearSummary(year: number, month: number, duration: number): Promise { + const filePath = getYearFilePath(year) + await ensureDirExists(filePath) + + let yearData = await this.getYearData(year) + + const monthStr = month.toString().padStart(2, '0') + if (!yearData.months[monthStr]) { + yearData.months[monthStr] = { totalDuration: 0, activeDays: 0 } + } + + yearData.months[monthStr].totalDuration += duration + yearData.yearlyTotal += duration + yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0) + + const monthCount = Object.keys(yearData.months).length + yearData.averageMonthly = Math.floor(yearData.yearlyTotal / monthCount) + yearData.averageDaily = yearData.totalActiveDays > 0 + ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) + : 0 + + await fs.writeFile(filePath, JSON.stringify(yearData, null, 2), 'utf-8') + } + + async recalculateMonthSummary(year: number, month: number, todayDuration: number): Promise { + const monthFilePath = getMonthFilePath(year, month) + await ensureDirExists(monthFilePath) + + let monthData = await this.getMonthData(year, month) + + const dayStr = new Date().getDate().toString().padStart(2, '0') + + if (!monthData.days[dayStr]) { + monthData.days[dayStr] = { totalDuration: 0, sessions: 0, topTabs: [] } + } + + const oldDayDuration = monthData.days[dayStr].totalDuration + monthData.days[dayStr].totalDuration = todayDuration + monthData.monthlyTotal = monthData.monthlyTotal - oldDayDuration + todayDuration + monthData.activeDays = Object.keys(monthData.days).length + monthData.averageDaily = monthData.activeDays > 0 + ? Math.floor(monthData.monthlyTotal / monthData.activeDays) + : 0 + monthData.lastUpdated = new Date().toISOString() + + await fs.writeFile(monthFilePath, JSON.stringify(monthData, null, 2), 'utf-8') + } + + async recalculateYearSummary(year: number): Promise { + const yearFilePath = getYearFilePath(year) + await ensureDirExists(yearFilePath) + + let yearData = await this.getYearData(year) + + const monthStr = (new Date().getMonth() + 1).toString().padStart(2, '0') + const monthFilePath = getMonthFilePath(year, new Date().getMonth() + 1) + + try { + const monthContent = await fs.readFile(monthFilePath, 'utf-8') + const monthData = JSON.parse(monthContent) + + if (!yearData.months[monthStr]) { + yearData.months[monthStr] = { totalDuration: 0, activeDays: 0 } + } + + const oldMonthTotal = yearData.months[monthStr].totalDuration + yearData.months[monthStr].totalDuration = monthData.monthlyTotal + yearData.months[monthStr].activeDays = monthData.activeDays + yearData.yearlyTotal = yearData.yearlyTotal - oldMonthTotal + monthData.monthlyTotal + yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0) + + const monthCount = Object.keys(yearData.months).length + yearData.averageMonthly = monthCount > 0 ? Math.floor(yearData.yearlyTotal / monthCount) : 0 + yearData.averageDaily = yearData.totalActiveDays > 0 + ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) + : 0 + } catch (err) { + logger.debug('Month file not found for year summary calculation') + } + + await fs.writeFile(yearFilePath, JSON.stringify(yearData, null, 2), 'utf-8') + } +} + +export const createSessionPersistence = (): SessionPersistence => { + return new SessionPersistenceService() +} + +export { SessionPersistenceService } diff --git a/api/modules/time-tracking/timeService.ts b/api/modules/time-tracking/timeService.ts new file mode 100644 index 0000000..4c9f31f --- /dev/null +++ b/api/modules/time-tracking/timeService.ts @@ -0,0 +1,442 @@ +import type { + DayTimeData, + MonthTimeData, + YearTimeData, + TimingSession, + TabRecord, + TabType, + TimeTrackingEvent, +} from '../../../shared/types.js' +import { getTabTypeFromPath, getFileNameFromPath } from '../../../shared/utils/tabType.js' +import { logger } from '../../utils/logger.js' +import { HeartbeatService, createHeartbeatService } from './heartbeatService.js' +import { SessionPersistence, createSessionPersistence } from './sessionPersistence.js' + +const generateId = (): string => { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` +} + +export interface TimeTrackerServiceDependencies { + heartbeatService: HeartbeatService + persistence: SessionPersistence +} + +export interface TimeTrackerServiceConfig { + heartbeatIntervalMs?: number +} + +class TimeTrackerService { + private currentSession: TimingSession | null = null + private currentTabRecord: TabRecord | null = null + private isPaused: boolean = false + private todayDuration: number = 0 + private _initialized: boolean = false + private static _initializationPromise: Promise | null = null + + private readonly heartbeatService: HeartbeatService + private readonly persistence: SessionPersistence + + private constructor( + dependencies: TimeTrackerServiceDependencies + ) { + this.heartbeatService = dependencies.heartbeatService + this.persistence = dependencies.persistence + } + + static async create( + config?: TimeTrackerServiceConfig + ): Promise { + const heartbeatService = createHeartbeatService(config?.heartbeatIntervalMs) + const persistence = createSessionPersistence() + + const instance = new TimeTrackerService({ + heartbeatService, + persistence + }) + + await instance.initialize() + return instance + } + + static async createWithDependencies( + dependencies: TimeTrackerServiceDependencies + ): Promise { + const instance = new TimeTrackerService(dependencies) + await instance.initialize() + return instance + } + + private async initialize(): Promise { + if (this._initialized) { + return + } + + if (TimeTrackerService._initializationPromise) { + await TimeTrackerService._initializationPromise + return + } + + TimeTrackerService._initializationPromise = this.loadCurrentState() + await TimeTrackerService._initializationPromise + this._initialized = true + TimeTrackerService._initializationPromise = null + + this.heartbeatService.setCallback(async () => { + if (this.currentSession && !this.isPaused) { + try { + this.heartbeatService.updateHeartbeat() + await this.updateCurrentTabDuration() + await this.saveCurrentState() + await this.updateTodayDataRealtime() + } catch (err) { + logger.error('Heartbeat update failed:', err) + } + } + }) + } + + ensureInitialized(): void { + if (!this._initialized) { + throw new Error('TimeTrackerService 未初始化,请使用 TimeTrackerService.create() 创建实例') + } + } + + private async loadCurrentState(): Promise { + const now = new Date() + const todayData = await this.persistence.getDayData(now.getFullYear(), now.getMonth() + 1, now.getDate()) + this.todayDuration = todayData.totalDuration + + const state = await this.persistence.loadCurrentState() + if (state.session && state.session.status === 'active') { + const sessionStart = new Date(state.session.startTime) + const now = new Date() + if (now.getTime() - sessionStart.getTime() < 24 * 60 * 60 * 1000) { + this.currentSession = state.session + this.isPaused = state.isPaused + if (state.currentTabRecord) { + this.currentTabRecord = state.currentTabRecord + } + this.heartbeatService.restoreState({ lastHeartbeat: state.lastHeartbeat }) + } else { + await this.endSession() + } + } + } + + private async saveCurrentState(): Promise { + await this.persistence.saveCurrentState({ + session: this.currentSession, + currentTabRecord: this.currentTabRecord, + isPaused: this.isPaused, + lastHeartbeat: this.heartbeatService.getLastHeartbeat().toISOString() + }) + } + + async startSession(): Promise { + if (this.currentSession && this.currentSession.status === 'active') { + return this.currentSession + } + + const now = new Date() + this.currentSession = { + id: generateId(), + startTime: now.toISOString(), + duration: 0, + status: 'active', + tabRecords: [] + } + this.isPaused = false + this.heartbeatService.updateHeartbeat() + + this.heartbeatService.start() + await this.saveCurrentState() + + return this.currentSession + } + + async pauseSession(): Promise { + if (!this.currentSession || this.isPaused) return + + this.isPaused = true + await this.updateCurrentTabDuration() + await this.saveCurrentState() + } + + async resumeSession(): Promise { + if (!this.currentSession || !this.isPaused) return + + this.isPaused = false + this.heartbeatService.updateHeartbeat() + + if (this.currentTabRecord) { + const now = new Date() + const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` + this.currentTabRecord.focusedPeriods.push({ start: timeStr, end: timeStr }) + } + + await this.saveCurrentState() + } + + async endSession(): Promise { + if (!this.currentSession) return + + this.heartbeatService.stop() + + await this.updateCurrentTabDuration() + + const now = new Date() + this.currentSession.endTime = now.toISOString() + this.currentSession.status = 'ended' + + const startTime = new Date(this.currentSession.startTime) + this.currentSession.duration = Math.floor((now.getTime() - startTime.getTime()) / 1000) + + await this.persistence.saveSessionToDay(this.currentSession) + + this.todayDuration += this.currentSession.duration + + this.currentSession = null + this.currentTabRecord = null + this.isPaused = false + + await this.persistence.clearCurrentState() + } + + private async updateCurrentTabDuration(): Promise { + if (!this.currentSession || !this.currentTabRecord) return + + const now = new Date() + const periods = this.currentTabRecord.focusedPeriods + + if (periods.length > 0) { + const lastPeriod = periods[periods.length - 1] + const [h, m, s] = lastPeriod.start.split(':').map(Number) + const startSeconds = h * 3600 + m * 60 + s + const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + + this.currentTabRecord.duration = currentSeconds - startSeconds + lastPeriod.end = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` + } + } + + private async updateTodayDataRealtime(): Promise { + if (!this.currentSession) return + + const now = new Date() + const year = now.getFullYear() + const month = now.getMonth() + 1 + const day = now.getDate() + + const dayData = await this.persistence.updateDayDataRealtime( + year, + month, + day, + this.currentSession, + this.currentTabRecord + ) + + this.todayDuration = dayData.totalDuration + + await this.persistence.recalculateMonthSummary(year, month, this.todayDuration) + await this.persistence.recalculateYearSummary(year) + } + + async handleTabSwitch(tabInfo: { tabId: string; filePath: string | null }): Promise { + if (!this.currentSession || this.isPaused) return + + await this.updateCurrentTabDuration() + + if (this.currentTabRecord && this.currentTabRecord.duration > 0) { + this.currentSession.tabRecords.push({ ...this.currentTabRecord }) + } + + const now = new Date() + const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` + + this.currentTabRecord = { + tabId: tabInfo.tabId, + filePath: tabInfo.filePath, + fileName: getFileNameFromPath(tabInfo.filePath), + tabType: getTabTypeFromPath(tabInfo.filePath), + duration: 0, + focusedPeriods: [{ start: timeStr, end: timeStr }] + } + + await this.saveCurrentState() + } + + async handleEvent(event: TimeTrackingEvent): Promise { + switch (event.type) { + case 'window-focus': + if (!this.currentSession) { + await this.startSession() + if (event.tabInfo) { + await this.handleTabSwitch(event.tabInfo) + } + } else { + await this.resumeSession() + await this.updateTodayDataRealtime() + } + break + case 'window-blur': + await this.pauseSession() + await this.updateTodayDataRealtime() + break + case 'app-quit': + await this.endSession() + break + case 'tab-switch': + case 'tab-open': + if (!this.currentSession) { + await this.startSession() + } + if (event.tabInfo) { + await this.handleTabSwitch(event.tabInfo) + } + await this.updateTodayDataRealtime() + break + case 'tab-close': + await this.updateCurrentTabDuration() + await this.updateTodayDataRealtime() + break + case 'heartbeat': + if (this.currentSession && !this.isPaused) { + this.heartbeatService.updateHeartbeat() + await this.updateCurrentTabDuration() + await this.saveCurrentState() + await this.updateTodayDataRealtime() + } + break + } + } + + async getDayData(year: number, month: number, day: number): Promise { + return this.persistence.getDayData(year, month, day) + } + + async getWeekData(startDate: Date): Promise { + const result: DayTimeData[] = [] + for (let i = 0; i < 7; i++) { + const date = new Date(startDate) + date.setDate(date.getDate() + i) + const data = await this.persistence.getDayData(date.getFullYear(), date.getMonth() + 1, date.getDate()) + result.push(data) + } + return result + } + + async getMonthData(year: number, month: number): Promise { + return this.persistence.getMonthData(year, month) + } + + async getYearData(year: number): Promise { + return this.persistence.getYearData(year) + } + + getCurrentState(): { isRunning: boolean; isPaused: boolean; currentSession: TimingSession | null; todayDuration: number; currentTabRecord: TabRecord | null } { + return { + isRunning: this.currentSession !== null, + isPaused: this.isPaused, + currentSession: this.currentSession, + todayDuration: this.todayDuration, + currentTabRecord: this.currentTabRecord + } + } + + async getStats(year?: number, month?: number): Promise<{ + totalDuration: number + activeDays: number + averageDaily: number + longestDay: { date: string; duration: number } | null + longestSession: { date: string; duration: number } | null + topTabs: Array<{ fileName: string; duration: number; percentage: number }> + tabTypeDistribution: Array<{ tabType: TabType; duration: number; percentage: number }> + }> { + const now = new Date() + const targetYear = year || now.getFullYear() + const targetMonth = month + + let totalDuration = 0 + let activeDays = 0 + let longestDay: { date: string; duration: number } | null = null + let longestSession: { date: string; duration: number } | null = null + const tabDurations: Record = {} + const tabTypeDurations: Record = {} as Record + + if (targetMonth) { + const monthData = await this.persistence.getMonthData(targetYear, targetMonth) + totalDuration = monthData.monthlyTotal + activeDays = monthData.activeDays + + for (const [day, summary] of Object.entries(monthData.days)) { + if (!longestDay || summary.totalDuration > longestDay.duration) { + longestDay = { date: `${targetYear}-${targetMonth.toString().padStart(2, '0')}-${day}`, duration: summary.totalDuration } + } + } + } else { + const yearData = await this.persistence.getYearData(targetYear) + totalDuration = yearData.yearlyTotal + activeDays = yearData.totalActiveDays + + for (const [month, summary] of Object.entries(yearData.months)) { + if (!longestDay || summary.totalDuration > longestDay.duration) { + longestDay = { date: `${targetYear}-${month}`, duration: summary.totalDuration } + } + } + } + + return { + totalDuration, + activeDays, + averageDaily: activeDays > 0 ? Math.floor(totalDuration / activeDays) : 0, + longestDay, + longestSession, + topTabs: Object.entries(tabDurations) + .map(([fileName, duration]) => ({ + fileName, + duration, + percentage: totalDuration > 0 ? Math.round((duration / totalDuration) * 100) : 0 + })) + .sort((a, b) => b.duration - a.duration) + .slice(0, 10), + tabTypeDistribution: Object.entries(tabTypeDurations) + .map(([tabType, duration]) => ({ + tabType: tabType as TabType, + duration, + percentage: totalDuration > 0 ? Math.round((duration / totalDuration) * 100) : 0 + })) + .sort((a, b) => b.duration - a.duration) + } + } +} + +let _timeTrackerService: TimeTrackerService | null = null + +export const getTimeTrackerService = (): TimeTrackerService => { + if (!_timeTrackerService) { + throw new Error('TimeTrackerService 未初始化,请先调用 initializeTimeTrackerService()') + } + return _timeTrackerService +} + +export const initializeTimeTrackerService = async ( + config?: TimeTrackerServiceConfig +): Promise => { + if (_timeTrackerService) { + return _timeTrackerService + } + _timeTrackerService = await TimeTrackerService.create(config) + return _timeTrackerService +} + +export const initializeTimeTrackerServiceWithDependencies = async ( + dependencies: TimeTrackerServiceDependencies +): Promise => { + if (_timeTrackerService) { + return _timeTrackerService + } + _timeTrackerService = await TimeTrackerService.createWithDependencies(dependencies) + return _timeTrackerService +} + +export { TimeTrackerService } diff --git a/api/modules/todo/__tests__/parser.test.ts b/api/modules/todo/__tests__/parser.test.ts new file mode 100644 index 0000000..a2f73e6 --- /dev/null +++ b/api/modules/todo/__tests__/parser.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect } from 'vitest' +import { parseTodoContent, generateTodoContent } from '../parser.js' +import type { DayTodo } from '../types.js' + +describe('parseTodoContent', () => { + it('should parse basic todo content correctly', () => { + const content = `## 2024-01-01 +- √ 完成工作 +- ○ 购物` + + const result = parseTodoContent(content) + + expect(result).toHaveLength(1) + expect(result[0].date).toBe('2024-01-01') + expect(result[0].items).toHaveLength(2) + expect(result[0].items[0].content).toBe('完成工作') + expect(result[0].items[1].content).toBe('购物') + }) + + it('should correctly identify completed status with √', () => { + const content = `## 2024-01-01 +- √ 已完成任务` + + const result = parseTodoContent(content) + + expect(result[0].items[0].completed).toBe(true) + }) + + it('should correctly identify incomplete status with ○', () => { + const content = `## 2024-01-01 +- ○ 未完成任务` + + const result = parseTodoContent(content) + + expect(result[0].items[0].completed).toBe(false) + }) + + it('should parse multiple days correctly', () => { + const content = `## 2024-01-01 +- √ 第一天任务 + +## 2024-01-02 +- ○ 第二天任务 + +## 2024-01-03 +- √ 第三天任务` + + const result = parseTodoContent(content) + + expect(result).toHaveLength(3) + expect(result[0].date).toBe('2024-01-01') + expect(result[1].date).toBe('2024-01-02') + expect(result[2].date).toBe('2024-01-03') + expect(result[0].items[0].content).toBe('第一天任务') + expect(result[1].items[0].content).toBe('第二天任务') + expect(result[2].items[0].content).toBe('第三天任务') + }) + + it('should handle empty content', () => { + const content = '' + + const result = parseTodoContent(content) + + expect(result).toHaveLength(0) + }) + + it('should ignore invalid format lines', () => { + const content = `## 2024-01-01 +这是一行普通文本 +- 无效格式 +- x 错误的标记 +random line +- √ 有效的任务` + + const result = parseTodoContent(content) + + expect(result).toHaveLength(1) + expect(result[0].items).toHaveLength(1) + expect(result[0].items[0].content).toBe('有效的任务') + }) + + it('should generate unique IDs for items', () => { + const content = `## 2024-01-01 +- √ 任务一 +- ○ 任务二 +- √ 任务三` + + const result = parseTodoContent(content) + + expect(result[0].items[0].id).toBe('2024-01-01-0') + expect(result[0].items[1].id).toBe('2024-01-01-1') + expect(result[0].items[2].id).toBe('2024-01-01-2') + }) +}) + +describe('generateTodoContent', () => { + it('should generate basic todo content correctly', () => { + const dayTodos: DayTodo[] = [ + { + date: '2024-01-01', + items: [ + { id: '2024-01-01-0', content: '完成工作', completed: true }, + { id: '2024-01-01-1', content: '购物', completed: false } + ] + } + ] + + const result = generateTodoContent(dayTodos) + + expect(result).toBe(`## 2024-01-01 +- √ 完成工作 +- ○ 购物`) + }) + + it('should include completed status in generated content', () => { + const dayTodos: DayTodo[] = [ + { + date: '2024-01-01', + items: [ + { id: '2024-01-01-0', content: '已完成', completed: true }, + { id: '2024-01-01-1', content: '未完成', completed: false } + ] + } + ] + + const result = generateTodoContent(dayTodos) + + expect(result).toContain('√ 已完成') + expect(result).toContain('○ 未完成') + }) + + it('should sort dates in ascending order', () => { + const dayTodos: DayTodo[] = [ + { date: '2024-01-03', items: [{ id: '1', content: '第三天', completed: false }] }, + { date: '2024-01-01', items: [{ id: '2', content: '第一天', completed: false }] }, + { date: '2024-01-02', items: [{ id: '3', content: '第二天', completed: false }] } + ] + + const result = generateTodoContent(dayTodos) + + const firstDateIndex = result.indexOf('2024-01-01') + const secondDateIndex = result.indexOf('2024-01-02') + const thirdDateIndex = result.indexOf('2024-01-03') + + expect(firstDateIndex).toBeLessThan(secondDateIndex) + expect(secondDateIndex).toBeLessThan(thirdDateIndex) + }) + + it('should handle empty array', () => { + const dayTodos: DayTodo[] = [] + + const result = generateTodoContent(dayTodos) + + expect(result).toBe('') + }) + + it('should generate content for multiple days with sorting', () => { + const dayTodos: DayTodo[] = [ + { date: '2024-01-02', items: [{ id: '1', content: '第二天', completed: true }] }, + { date: '2024-01-01', items: [{ id: '2', content: '第一天', completed: false }] } + ] + + const result = generateTodoContent(dayTodos) + + expect(result).toBe(`## 2024-01-01 +- ○ 第一天 + +## 2024-01-02 +- √ 第二天`) + }) +}) diff --git a/api/modules/todo/index.ts b/api/modules/todo/index.ts new file mode 100644 index 0000000..4e73b02 --- /dev/null +++ b/api/modules/todo/index.ts @@ -0,0 +1,28 @@ +import type { Router } from 'express' +import type { ServiceContainer } from '../../infra/container.js' +import { createApiModule } from '../../infra/createModule.js' +import { TODO_MODULE } from '../../../shared/modules/todo/index.js' +import { TodoService } from './service.js' +import { createTodoRoutes } from './routes.js' + +export * from './types.js' +export * from './parser.js' +export * from './service.js' +export * from './schemas.js' +export * from './routes.js' + +export const createTodoModule = () => { + return createApiModule(TODO_MODULE, { + routes: (container: ServiceContainer): Router => { + const todoService = container.getSync('todoService') + return createTodoRoutes({ todoService }) + }, + lifecycle: { + onLoad: (container: ServiceContainer): void => { + container.register('todoService', () => new TodoService()) + }, + }, + }) +} + +export default createTodoModule diff --git a/api/modules/todo/parser.ts b/api/modules/todo/parser.ts new file mode 100644 index 0000000..5d19b40 --- /dev/null +++ b/api/modules/todo/parser.ts @@ -0,0 +1,51 @@ +import type { TodoItem, DayTodo } from './types.js' + +export const parseTodoContent = (content: string): DayTodo[] => { + const lines = content.split('\n') + const result: DayTodo[] = [] + let currentDate: string | null = null + let currentItems: TodoItem[] = [] + let itemId = 0 + + for (const line of lines) { + const dateMatch = line.match(/^##\s*(\d{4}-\d{2}-\d{2})/) + if (dateMatch) { + if (currentDate) { + result.push({ date: currentDate, items: currentItems }) + } + currentDate = dateMatch[1] + currentItems = [] + } else if (currentDate) { + const todoMatch = line.match(/^- (√|○) (.*)$/) + if (todoMatch) { + currentItems.push({ + id: `${currentDate}-${itemId++}`, + content: todoMatch[2], + completed: todoMatch[1] === '√' + }) + } + } + } + + if (currentDate) { + result.push({ date: currentDate, items: currentItems }) + } + + return result +} + +export const generateTodoContent = (dayTodos: DayTodo[]): string => { + const lines: string[] = [] + const sortedDays = [...dayTodos].sort((a, b) => a.date.localeCompare(b.date)) + + for (const day of sortedDays) { + lines.push(`## ${day.date}`) + for (const item of day.items) { + const checkbox = item.completed ? '√' : '○' + lines.push(`- ${checkbox} ${item.content}`) + } + lines.push('') + } + + return lines.join('\n').trimEnd() +} diff --git a/api/modules/todo/routes.ts b/api/modules/todo/routes.ts new file mode 100644 index 0000000..d8e83df --- /dev/null +++ b/api/modules/todo/routes.ts @@ -0,0 +1,99 @@ +import express, { type Request, type Response, type Router } from 'express' +import { asyncHandler } from '../../utils/asyncHandler.js' +import { successResponse } from '../../utils/response.js' +import { validateBody, validateQuery } from '../../middlewares/validate.js' +import { TodoService } from './service.js' +import { + getTodoQuerySchema, + saveTodoSchema, + addTodoSchema, + toggleTodoSchema, + updateTodoSchema, + deleteTodoSchema, +} from './schemas.js' + +export interface TodoRoutesDependencies { + todoService: TodoService +} + +export const createTodoRoutes = (deps: TodoRoutesDependencies): Router => { + const router = express.Router() + const { todoService } = deps + + router.get( + '/', + validateQuery(getTodoQuerySchema), + asyncHandler(async (req: Request, res: Response) => { + const year = parseInt(req.query.year as string) || new Date().getFullYear() + const month = parseInt(req.query.month as string) || new Date().getMonth() + 1 + + const result = await todoService.getTodo(year, month) + successResponse(res, result) + }), + ) + + router.post( + '/save', + validateBody(saveTodoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { year, month, dayTodos } = req.body + + await todoService.saveTodo(year, month, dayTodos) + + successResponse(res, null) + }), + ) + + router.post( + '/add', + validateBody(addTodoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { year, month, date, content: todoContent } = req.body + + const dayTodos = await todoService.addTodo(year, month, date, todoContent) + + successResponse(res, { dayTodos }) + }), + ) + + router.post( + '/toggle', + validateBody(toggleTodoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { year, month, date, itemIndex, completed } = req.body + + const dayTodos = await todoService.toggleTodo(year, month, date, itemIndex, completed) + + successResponse(res, { dayTodos }) + }), + ) + + router.post( + '/update', + validateBody(updateTodoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { year, month, date, itemIndex, content: newContent } = req.body + + const dayTodos = await todoService.updateTodo(year, month, date, itemIndex, newContent) + + successResponse(res, { dayTodos }) + }), + ) + + router.delete( + '/delete', + validateBody(deleteTodoSchema), + asyncHandler(async (req: Request, res: Response) => { + const { year, month, date, itemIndex } = req.body + + const dayTodos = await todoService.deleteTodo(year, month, date, itemIndex) + + successResponse(res, { dayTodos }) + }), + ) + + return router +} + +const todoService = new TodoService() +export default createTodoRoutes({ todoService }) diff --git a/api/modules/todo/schemas.ts b/api/modules/todo/schemas.ts new file mode 100644 index 0000000..d2be2d8 --- /dev/null +++ b/api/modules/todo/schemas.ts @@ -0,0 +1,53 @@ +import { z } from 'zod' + +const todoItemSchema = z.object({ + id: z.string(), + content: z.string(), + completed: z.boolean(), +}) + +const dayTodoSchema = z.object({ + date: z.string(), + items: z.array(todoItemSchema), +}) + +export const getTodoQuerySchema = z.object({ + year: z.string().optional(), + month: z.string().optional(), +}) + +export const saveTodoSchema = z.object({ + year: z.number().int().positive(), + month: z.number().int().min(1).max(12), + dayTodos: z.array(dayTodoSchema), +}) + +export const addTodoSchema = z.object({ + year: z.number().int().positive(), + month: z.number().int().min(1).max(12), + date: z.string(), + content: z.string(), +}) + +export const toggleTodoSchema = z.object({ + year: z.number().int().positive(), + month: z.number().int().min(1).max(12), + date: z.string(), + itemIndex: z.number().int().nonnegative(), + completed: z.boolean(), +}) + +export const updateTodoSchema = z.object({ + year: z.number().int().positive(), + month: z.number().int().min(1).max(12), + date: z.string(), + itemIndex: z.number().int().nonnegative(), + content: z.string(), +}) + +export const deleteTodoSchema = z.object({ + year: z.number().int().positive(), + month: z.number().int().min(1).max(12), + date: z.string(), + itemIndex: z.number().int().nonnegative(), +}) diff --git a/api/modules/todo/service.ts b/api/modules/todo/service.ts new file mode 100644 index 0000000..c7e4ef0 --- /dev/null +++ b/api/modules/todo/service.ts @@ -0,0 +1,216 @@ +import fs from 'fs/promises' +import path from 'path' +import { resolveNotebookPath } from '../../utils/pathSafety.js' +import { NotFoundError } from '../../../shared/errors/index.js' +import { parseTodoContent, generateTodoContent } from './parser.js' +import type { DayTodo, TodoFilePath, ParsedTodoFile, GetTodoResult } from './types.js' + +export interface TodoServiceDependencies { +} + +export class TodoService { + constructor(private deps: TodoServiceDependencies = {}) {} + + getTodoFilePath(year: number, month: number): TodoFilePath { + const yearStr = year.toString() + const monthStr = month.toString().padStart(2, '0') + const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md` + const { fullPath } = resolveNotebookPath(relPath) + return { relPath, fullPath } + } + + async ensureTodoFileExists(fullPath: string): Promise { + const dir = path.dirname(fullPath) + await fs.mkdir(dir, { recursive: true }) + try { + await fs.access(fullPath) + } catch { + await fs.writeFile(fullPath, '', 'utf-8') + } + } + + async loadAndParseTodoFile(year: number, month: number): Promise { + const { fullPath } = this.getTodoFilePath(year, month) + try { + await fs.access(fullPath) + } catch { + throw new NotFoundError('TODO file not found') + } + const content = await fs.readFile(fullPath, 'utf-8') + return { fullPath, dayTodos: parseTodoContent(content) } + } + + async saveTodoFile(fullPath: string, dayTodos: DayTodo[]): Promise { + const content = generateTodoContent(dayTodos) + await fs.writeFile(fullPath, content, 'utf-8') + } + + async getTodo(year: number, month: number): Promise { + const { fullPath } = this.getTodoFilePath(year, month) + + let dayTodos: DayTodo[] = [] + try { + await fs.access(fullPath) + const content = await fs.readFile(fullPath, 'utf-8') + dayTodos = parseTodoContent(content) + } catch { + // 文件不存在 + } + + const now = new Date() + const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}` + + const yesterday = new Date(now) + yesterday.setDate(yesterday.getDate() - 1) + const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, '0')}-${yesterday.getDate().toString().padStart(2, '0')}` + + if (year === now.getFullYear() && month === now.getMonth() + 1) { + const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr) + + if (migrated) { + const newContent = generateTodoContent(dayTodos) + await this.ensureTodoFileExists(fullPath) + await fs.writeFile(fullPath, newContent, 'utf-8') + } + } + + return { dayTodos, year, month } + } + + private migrateIncompleteItems(dayTodos: DayTodo[], todayStr: string, yesterdayStr: string): boolean { + let migrated = false + + const yesterdayTodo = dayTodos.find(d => d.date === yesterdayStr) + if (yesterdayTodo) { + const incompleteItems = yesterdayTodo.items.filter(item => !item.completed) + if (incompleteItems.length > 0) { + const todayTodo = dayTodos.find(d => d.date === todayStr) + if (todayTodo) { + const existingIds = new Set(todayTodo.items.map(i => i.id)) + const itemsToAdd = incompleteItems.map((item, idx) => ({ + ...item, + id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id + })) + todayTodo.items = [...itemsToAdd, ...todayTodo.items] + } else { + dayTodos.push({ + date: todayStr, + items: incompleteItems.map((item, idx) => ({ + ...item, + id: `${todayStr}-migrated-${idx}` + })) + }) + } + + yesterdayTodo.items = yesterdayTodo.items.filter(item => item.completed) + if (yesterdayTodo.items.length === 0) { + const index = dayTodos.findIndex(d => d.date === yesterdayStr) + if (index !== -1) { + dayTodos.splice(index, 1) + } + } + + migrated = true + } + } + + return migrated + } + + async saveTodo(year: number, month: number, dayTodos: DayTodo[]): Promise { + const { fullPath } = this.getTodoFilePath(year, month) + await this.ensureTodoFileExists(fullPath) + const content = generateTodoContent(dayTodos) + await fs.writeFile(fullPath, content, 'utf-8') + } + + async addTodo(year: number, month: number, date: string, todoContent: string): Promise { + const { fullPath } = this.getTodoFilePath(year, month) + await this.ensureTodoFileExists(fullPath) + + let fileContent = await fs.readFile(fullPath, 'utf-8') + const dayTodos = parseTodoContent(fileContent) + + const existingDay = dayTodos.find(d => d.date === date) + if (existingDay) { + const newId = `${date}-${existingDay.items.length}` + existingDay.items.push({ + id: newId, + content: todoContent, + completed: false + }) + } else { + dayTodos.push({ + date, + items: [{ + id: `${date}-0`, + content: todoContent, + completed: false + }] + }) + } + + fileContent = generateTodoContent(dayTodos) + await fs.writeFile(fullPath, fileContent, 'utf-8') + + return dayTodos + } + + async toggleTodo(year: number, month: number, date: string, itemIndex: number, completed: boolean): Promise { + const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month) + + const day = dayTodos.find(d => d.date === date) + if (!day || itemIndex >= day.items.length) { + throw new NotFoundError('TODO item not found') + } + + day.items[itemIndex].completed = completed + + await this.saveTodoFile(fullPath, dayTodos) + + return dayTodos + } + + async updateTodo(year: number, month: number, date: string, itemIndex: number, newContent: string): Promise { + const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month) + + const day = dayTodos.find(d => d.date === date) + if (!day || itemIndex >= day.items.length) { + throw new NotFoundError('TODO item not found') + } + + day.items[itemIndex].content = newContent + + await this.saveTodoFile(fullPath, dayTodos) + + return dayTodos + } + + async deleteTodo(year: number, month: number, date: string, itemIndex: number): Promise { + const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month) + + const dayIndex = dayTodos.findIndex(d => d.date === date) + if (dayIndex === -1) { + throw new NotFoundError('Day not found') + } + + const day = dayTodos[dayIndex] + if (itemIndex >= day.items.length) { + throw new NotFoundError('TODO item not found') + } + + day.items.splice(itemIndex, 1) + + if (day.items.length === 0) { + dayTodos.splice(dayIndex, 1) + } + + await this.saveTodoFile(fullPath, dayTodos) + + return dayTodos + } +} + +export const createTodoService = (deps?: TodoServiceDependencies): TodoService => { + return new TodoService(deps) +} diff --git a/api/modules/todo/types.ts b/api/modules/todo/types.ts new file mode 100644 index 0000000..c2d9e7b --- /dev/null +++ b/api/modules/todo/types.ts @@ -0,0 +1,7 @@ +export { type TodoItem, type DayTodo } from '../../../shared/types/todo.js' +export { + type TodoFilePath, + type ParsedTodoFile, + type GetTodoResult, + type MigrationContext, +} from '../../../shared/modules/todo/types.js' diff --git a/api/schemas/files.ts b/api/schemas/files.ts new file mode 100644 index 0000000..cb612fb --- /dev/null +++ b/api/schemas/files.ts @@ -0,0 +1,43 @@ +import { z } from 'zod' + +export const listFilesQuerySchema = z.object({ + path: z.string().optional().default(''), +}) + +export const contentQuerySchema = z.object({ + path: z.string().min(1), +}) + +export const rawQuerySchema = z.object({ + path: z.string().min(1), +}) + +export const pathSchema = z.object({ + path: z.string().min(1), +}) + +export const saveFileSchema = z.object({ + path: z.string().min(1), + content: z.string(), +}) + +export const renameSchema = z.object({ + oldPath: z.string().min(1), + newPath: z.string().min(1), +}) + +export const searchSchema = z.object({ + keywords: z.array(z.string()).min(1), +}) + +export const existsSchema = z.object({ + path: z.string().min(1), +}) + +export const createDirSchema = z.object({ + path: z.string().min(1), +}) + +export const createFileSchema = z.object({ + path: z.string().min(1), +}) diff --git a/api/schemas/index.ts b/api/schemas/index.ts new file mode 100644 index 0000000..ea267c1 --- /dev/null +++ b/api/schemas/index.ts @@ -0,0 +1,2 @@ +export * from './files.js' +export * from './pydemos.js' diff --git a/api/schemas/pydemos.ts b/api/schemas/pydemos.ts new file mode 100644 index 0000000..6f38367 --- /dev/null +++ b/api/schemas/pydemos.ts @@ -0,0 +1,21 @@ +import { z } from 'zod' + +export const listPyDemosQuerySchema = z.object({ + year: z.string().optional(), +}) + +export const createPyDemoSchema = z.object({ + name: z.string().min(1), + year: z.string().min(1), + month: z.string().min(1), + folderStructure: z.string().optional(), +}) + +export const deletePyDemoSchema = z.object({ + path: z.string().min(1), +}) + +export const renamePyDemoSchema = z.object({ + oldPath: z.string().min(1), + newName: z.string().min(1), +}) diff --git a/api/server.ts b/api/server.ts new file mode 100644 index 0000000..3043003 --- /dev/null +++ b/api/server.ts @@ -0,0 +1,48 @@ +import app, { moduleManager, container } from './app.js'; +import { startWatcher, stopWatcher } from './watcher/watcher.js'; +import { logger } from './utils/logger.js'; + +const PORT = process.env.PORT || 3001; + +startWatcher(); + +const server = app.listen(PORT, () => { + logger.info(`Server ready on port ${PORT}`); +}); + +async function gracefulShutdown(signal: string) { + logger.info(`${signal} signal received`); + + try { + await stopWatcher(); + } catch (error) { + logger.error('Error stopping watcher:', error); + } + + const activeModules = moduleManager.getActiveModules(); + for (const moduleId of activeModules.reverse()) { + try { + await moduleManager.deactivate(moduleId); + logger.info(`Module '${moduleId}' deactivated`); + } catch (error) { + logger.error(`Error deactivating module '${moduleId}':`, error); + } + } + + try { + await container.dispose(); + logger.info('Service container disposed'); + } catch (error) { + logger.error('Error disposing container:', error); + } + + server.close(() => { + logger.info('Server closed'); + process.exit(0); + }); +} + +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); +process.on('SIGINT', () => gracefulShutdown('SIGINT')); + +export default app; diff --git a/api/utils/__tests__/asyncHandler.test.ts b/api/utils/__tests__/asyncHandler.test.ts new file mode 100644 index 0000000..a5695e1 --- /dev/null +++ b/api/utils/__tests__/asyncHandler.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { asyncHandler } from '../asyncHandler' +import type { Request, Response, NextFunction } from 'express' + +describe('asyncHandler', () => { + const mockReq = {} as Request + const mockRes = {} as Response + const mockNext = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('成功调用', () => { + it('应正常执行函数并返回结果', async () => { + const mockHandler = vi.fn().mockResolvedValue('操作成功') + const wrappedHandler = asyncHandler(mockHandler) + + await wrappedHandler(mockReq, mockRes, mockNext) + + expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes, mockNext) + expect(mockNext).not.toHaveBeenCalled() + }) + + it('应处理返回同步值的函数', async () => { + const mockHandler = vi.fn().mockResolvedValue({ id: 1, name: 'test' }) + const wrappedHandler = asyncHandler(mockHandler) + + await wrappedHandler(mockReq, mockRes, mockNext) + + expect(mockHandler).toHaveBeenCalled() + expect(mockNext).not.toHaveBeenCalled() + }) + }) + + describe('异常传播', () => { + it('应正确传播异步错误', async () => { + const asyncError = new Error('异步错误') + const mockHandler = vi.fn().mockRejectedValue(asyncError) + const wrappedHandler = asyncHandler(mockHandler) + + await wrappedHandler(mockReq, mockRes, mockNext) + + expect(mockHandler).toHaveBeenCalled() + expect(mockNext).toHaveBeenCalledWith(asyncError) + }) + + it('应处理 Promise.reject 的错误', async () => { + const error = new Error('Promise rejected') + const mockHandler = vi.fn().mockReturnValue(Promise.reject(error)) + const wrappedHandler = asyncHandler(mockHandler) + + await wrappedHandler(mockReq, mockRes, mockNext) + + expect(mockNext).toHaveBeenCalledWith(error) + }) + + it('应处理非 Error 对象的异步错误', async () => { + const error = '字符串错误' + const mockHandler = vi.fn().mockRejectedValue(error) + const wrappedHandler = asyncHandler(mockHandler) + + await wrappedHandler(mockReq, mockRes, mockNext) + + expect(mockNext).toHaveBeenCalledWith(error) + }) + }) + + describe('函数调用时机', () => { + it('应立即调用底层函数', () => { + const mockHandler = vi.fn().mockResolvedValue('result') + const wrappedHandler = asyncHandler(mockHandler) + + wrappedHandler(mockReq, mockRes, mockNext) + + expect(mockHandler).toHaveBeenCalled() + }) + }) +}) diff --git a/api/utils/__tests__/response.test.ts b/api/utils/__tests__/response.test.ts new file mode 100644 index 0000000..57d0213 --- /dev/null +++ b/api/utils/__tests__/response.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, vi } from 'vitest' +import { successResponse, errorResponse } from '../response' +import type { Response } from 'express' + +vi.mock('express', () => ({ + default: {}, +})) + +describe('successResponse', () => { + it('应返回正确格式的成功响应(默认状态码)', () => { + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + const data = { message: '操作成功' } + successResponse(mockRes, data) + + expect(mockRes.status).toHaveBeenCalledWith(200) + expect(mockRes.json).toHaveBeenCalledWith({ + success: true, + data: { message: '操作成功' }, + }) + }) + + it('应使用自定义状态码', () => { + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + const data = { id: 123 } + successResponse(mockRes, data, 201) + + expect(mockRes.status).toHaveBeenCalledWith(201) + expect(mockRes.json).toHaveBeenCalledWith({ + success: true, + data: { id: 123 }, + }) + }) + + it('应正确处理数组数据', () => { + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + const data = [1, 2, 3] + successResponse(mockRes, data) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: true, + data: [1, 2, 3], + }) + }) +}) + +describe('errorResponse', () => { + it('应返回正确格式的错误响应', () => { + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + errorResponse(mockRes, 400, 'BAD_REQUEST', '请求参数错误') + + expect(mockRes.status).toHaveBeenCalledWith(400) + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'BAD_REQUEST', + message: '请求参数错误', + }, + }) + }) + + it('应在非生产环境包含 details', () => { + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + const details = { field: 'username', reason: 'required' } + errorResponse(mockRes, 422, 'VALIDATION_ERROR', '验证失败', details) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'VALIDATION_ERROR', + message: '验证失败', + details: { field: 'username', reason: 'required' }, + }, + }) + }) + + it('应在生产环境不包含 details', () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + const details = { field: 'username', reason: 'required' } + errorResponse(mockRes, 422, 'VALIDATION_ERROR', '验证失败', details) + + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'VALIDATION_ERROR', + message: '验证失败', + details: undefined, + }, + }) + + process.env.NODE_ENV = originalEnv + }) + + it('应正确处理不带 details 的错误响应', () => { + const mockRes = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + + errorResponse(mockRes, 404, 'NOT_FOUND', '资源不存在') + + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: { + code: 'NOT_FOUND', + message: '资源不存在', + }, + }) + }) +}) diff --git a/api/utils/asyncHandler.ts b/api/utils/asyncHandler.ts new file mode 100644 index 0000000..c8b93e5 --- /dev/null +++ b/api/utils/asyncHandler.ts @@ -0,0 +1,10 @@ +import type { NextFunction, Request, Response } from 'express' + +export const asyncHandler = + ( + fn: (req: TReq, res: TRes, next: NextFunction) => unknown | Promise, + ) => + (req: TReq, res: TRes, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next) + } + diff --git a/api/utils/file.ts b/api/utils/file.ts new file mode 100644 index 0000000..fa06119 --- /dev/null +++ b/api/utils/file.ts @@ -0,0 +1,126 @@ +import fs from 'fs/promises' +import path from 'path' +import { InternalError, ValidationError } from '../../shared/errors/index.js' + +export const getUniqueFilename = async (imagesDirFullPath: string, baseName: string, ext: string) => { + const maxAttempts = 1000 + for (let i = 0; i < maxAttempts; i++) { + const suffix = i === 0 ? '' : `-${i + 1}` + const filename = `${baseName}${suffix}${ext}` + const fullPath = path.join(imagesDirFullPath, filename) + try { + await fs.access(fullPath) + } catch { + return filename + } + } + throw new InternalError('Failed to generate unique filename') +} + +export const mimeToExt: Record = { + 'image/png': '.png', + 'image/jpeg': '.jpg', + 'image/jpg': '.jpg', + 'image/gif': '.gif', + 'image/webp': '.webp', +} + +const IMAGE_MAGIC_BYTES: Record = { + 'image/png': { bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }, + 'image/jpeg': { bytes: [0xff, 0xd8, 0xff] }, + 'image/gif': { bytes: [0x47, 0x49, 0x46, 0x38] }, + 'image/webp': { bytes: [0x52, 0x49, 0x46, 0x46], offset: 0 }, +} + +const WEBP_RIFF_HEADER = [0x52, 0x49, 0x46, 0x46] +const WEBP_WEBP_MARKER = [0x57, 0x45, 0x42, 0x50] + +const MIN_IMAGE_SIZE = 16 +const MAX_IMAGE_SIZE = 8 * 1024 * 1024 + +export const validateImageBuffer = (buffer: Buffer, claimedMimeType: string): void => { + if (buffer.byteLength < MIN_IMAGE_SIZE) { + throw new ValidationError('Image file is too small or corrupted') + } + + if (buffer.byteLength > MAX_IMAGE_SIZE) { + throw new ValidationError('Image file is too large') + } + + const magicInfo = IMAGE_MAGIC_BYTES[claimedMimeType] + if (!magicInfo) { + throw new ValidationError('Unsupported image type for content validation') + } + + const offset = magicInfo.offset || 0 + const expectedBytes = magicInfo.bytes + + for (let i = 0; i < expectedBytes.length; i++) { + if (buffer[offset + i] !== expectedBytes[i]) { + throw new ValidationError('Image content does not match the claimed file type') + } + } + + if (claimedMimeType === 'image/webp') { + if (buffer.byteLength < 12) { + throw new ValidationError('WebP image is corrupted') + } + for (let i = 0; i < WEBP_WEBP_MARKER.length; i++) { + if (buffer[8 + i] !== WEBP_WEBP_MARKER[i]) { + throw new ValidationError('WebP image content is invalid') + } + } + } +} + +export const detectImageMimeType = (buffer: Buffer): string | null => { + if (buffer.byteLength < 8) return null + + if ( + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 && + buffer[4] === 0x0d && + buffer[5] === 0x0a && + buffer[6] === 0x1a && + buffer[7] === 0x0a + ) { + return 'image/png' + } + + if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { + return 'image/jpeg' + } + + if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) { + return 'image/gif' + } + + if ( + buffer[0] === 0x52 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x46 && + buffer[8] === 0x57 && + buffer[9] === 0x45 && + buffer[10] === 0x42 && + buffer[11] === 0x50 + ) { + return 'image/webp' + } + + return null +} + +export const sanitizeFilename = (filename: string): string => { + let sanitized = filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') + sanitized = sanitized.replace(/^\.+|\.+$/g, '') + sanitized = sanitized.replace(/\.{2,}/g, '.') + if (sanitized.length > 200) { + const ext = path.extname(filename) + const baseName = path.basename(filename, ext) + sanitized = baseName.substring(0, 200 - ext.length) + ext + } + return sanitized || 'unnamed' +} diff --git a/api/utils/logger.ts b/api/utils/logger.ts new file mode 100644 index 0000000..eca48a2 --- /dev/null +++ b/api/utils/logger.ts @@ -0,0 +1,15 @@ +type LogFn = (...args: unknown[]) => void + +const createLogger = () => { + const isProd = process.env.NODE_ENV === 'production' + + const debug: LogFn = isProd ? () => {} : console.debug.bind(console) + const info: LogFn = console.info.bind(console) + const warn: LogFn = console.warn.bind(console) + const error: LogFn = console.error.bind(console) + + return { debug, info, warn, error } +} + +export const logger = createLogger() + diff --git a/api/utils/pathSafety.ts b/api/utils/pathSafety.ts new file mode 100644 index 0000000..7f62afd --- /dev/null +++ b/api/utils/pathSafety.ts @@ -0,0 +1,106 @@ +import path from 'path' +import fs from 'fs/promises' +import { NOTEBOOK_ROOT } from '../config/paths.js' +import { AccessDeniedError } from '../../shared/errors/index.js' + +const DANGEROUS_PATTERNS = [ + /\.\./, + /\0/, + /%2e%2e[%/]/i, + /%252e%252e[%/]/i, + /\.\.%2f/i, + /\.\.%5c/i, + /%c0%ae/i, + /%c1%9c/i, + /%c0%ae%c0%ae/i, + /%c1%9c%c1%9c/i, + /\.\.%c0%af/i, + /\.\.%c1%9c/i, + /%252e/i, + /%uff0e/i, + /%u002e/i, +] + +const WINDOWS_RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]|CLOCK\$)$/i + +const DOUBLE_ENCODE_PATTERNS = [ + /%25[0-9a-fA-F]{2}/, + /%[0-9a-fA-F]{2}%[0-9a-fA-F]{2}/, +] + +export const normalizeRelPath = (input: string) => { + const trimmed = input.replace(/\0/g, '').trim() + return trimmed.replace(/^[/\\]+/, '') +} + +export const containsPathTraversal = (input: string): boolean => { + const decoded = decodeURIComponentSafe(input) + return DANGEROUS_PATTERNS.some(pattern => pattern.test(input) || pattern.test(decoded)) +} + +export const containsDoubleEncoding = (input: string): boolean => { + return DOUBLE_ENCODE_PATTERNS.some(pattern => pattern.test(input)) +} + +export const hasPathSecurityIssues = (input: string): boolean => { + return containsPathTraversal(input) || containsDoubleEncoding(input) +} + +const decodeURIComponentSafe = (input: string): string => { + try { + return decodeURIComponent(input) + } catch { + return input + } +} + +export const resolveNotebookPath = (relPath: string) => { + if (hasPathSecurityIssues(relPath)) { + throw new AccessDeniedError('Path traversal detected') + } + + const safeRelPath = normalizeRelPath(relPath) + const notebookRoot = path.resolve(NOTEBOOK_ROOT) + const fullPath = path.resolve(notebookRoot, safeRelPath) + + if (!fullPath.startsWith(notebookRoot)) { + throw new AccessDeniedError('Access denied') + } + + return { safeRelPath, fullPath } +} + +export const resolveNotebookPathSafe = async (relPath: string) => { + if (hasPathSecurityIssues(relPath)) { + throw new AccessDeniedError('Path traversal detected') + } + + const safeRelPath = normalizeRelPath(relPath) + const notebookRoot = path.resolve(NOTEBOOK_ROOT) + const fullPath = path.resolve(notebookRoot, safeRelPath) + + try { + await fs.access(fullPath) + } catch { + return { safeRelPath, fullPath, realPath: null } + } + + const realFullPath = await fs.realpath(fullPath) + const realRoot = await fs.realpath(notebookRoot) + + if (!realFullPath.startsWith(realRoot)) { + throw new AccessDeniedError('Symbolic link escapes notebook root') + } + + return { safeRelPath, fullPath, realPath: realFullPath } +} + +export const validateFileName = (name: string): boolean => { + if (!name || name.length === 0) return false + if (name.length > 255) return false + const invalidChars = /[<>:"/\\|?*\x00-\x1f]/ + if (invalidChars.test(name)) return false + if (WINDOWS_RESERVED_NAMES.test(name)) return false + if (name.startsWith('.') || name.endsWith('.')) return false + return true +} diff --git a/api/utils/response.ts b/api/utils/response.ts new file mode 100644 index 0000000..78cf2cf --- /dev/null +++ b/api/utils/response.ts @@ -0,0 +1,34 @@ +import type { Response } from 'express' +import type { ApiResponse } from '../../shared/types.js' + +/** + * Send a successful API response + */ +export const successResponse = (res: Response, data: T, statusCode: number = 200): void => { + const response: ApiResponse = { + success: true, + data, + } + res.status(statusCode).json(response) +} + +/** + * Send an error API response + */ +export const errorResponse = ( + res: Response, + statusCode: number, + code: string, + message: string, + details?: unknown +): void => { + const response: ApiResponse = { + success: false, + error: { + code, + message, + details: process.env.NODE_ENV === 'production' ? undefined : details, + }, + } + res.status(statusCode).json(response) +} diff --git a/api/utils/tempDir.ts b/api/utils/tempDir.ts new file mode 100644 index 0000000..df4844b --- /dev/null +++ b/api/utils/tempDir.ts @@ -0,0 +1,23 @@ +import { existsSync, mkdirSync } from 'fs' +import path from 'path' +import { PATHS } from '../config/paths.js' + +let tempDir: string | null = null + +export const getTempDir = (): string => { + if (!tempDir) { + tempDir = PATHS.TEMP_ROOT + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }) + } + } + return tempDir +} + +export const getTempFilePath = (filename: string): string => { + return path.join(getTempDir(), filename) +} + +export const ensureTempDir = (): string => { + return getTempDir() +} diff --git a/api/watcher/watcher.ts b/api/watcher/watcher.ts new file mode 100644 index 0000000..93d7f0c --- /dev/null +++ b/api/watcher/watcher.ts @@ -0,0 +1,43 @@ +import chokidar, { FSWatcher } from 'chokidar'; +import path from 'path'; +import { NOTEBOOK_ROOT } from '../config/paths.js'; +import { eventBus } from '../events/eventBus.js'; +import { logger } from '../utils/logger.js'; +import { toPosixPath } from '../../shared/utils/path.js'; + +let watcher: FSWatcher | null = null; + +export const startWatcher = (): void => { + if (watcher) return; + + logger.info(`Starting file watcher for: ${NOTEBOOK_ROOT}`); + + watcher = chokidar.watch(NOTEBOOK_ROOT, { + ignored: /(^|[\/\\])\../, + persistent: true, + ignoreInitial: true, + }); + + const broadcast = (event: string, changedPath: string) => { + const rel = path.relative(NOTEBOOK_ROOT, changedPath); + if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return; + logger.info(`File event: ${event} - ${rel}`); + eventBus.broadcast({ event, path: toPosixPath(rel) }); + }; + + watcher + .on('add', (p) => broadcast('add', p)) + .on('change', (p) => broadcast('change', p)) + .on('unlink', (p) => broadcast('unlink', p)) + .on('addDir', (p) => broadcast('addDir', p)) + .on('unlinkDir', (p) => broadcast('unlinkDir', p)) + .on('ready', () => logger.info('File watcher ready')) + .on('error', (err) => logger.error('File watcher error:', err)); +}; + +export const stopWatcher = async (): Promise => { + if (watcher) { + await watcher.close(); + watcher = null; + } +}; diff --git a/command/Markdown整理要求.md b/command/Markdown整理要求.md new file mode 100644 index 0000000..6c97988 --- /dev/null +++ b/command/Markdown整理要求.md @@ -0,0 +1,141 @@ +# Markdown文本全维度修复+标准化整理+视觉化美化要求 +注意一定要按照章节来制定计划!!!!!!!!!!!!!!! +没让你翻译就别乱翻译,一定要保证原文内容不变!!!!!!!!!! +一定注意主章节要跟标题一样用一级# +>引用块里面的结构也需要注意,现在引用块里面的层级都被打乱了,需要你重新整理。 +## 一、任务拆分 + +### 1.1 章节划分 +严格根据文档原有章节划分处理单元,按章节依次推进修复、整理、美化工作,不一次性对全文档操作,单章节处理完成后再进行下一章节,跨章节格式保持完全统一。 + +### 1.2 操作原则 +直接在原文档中改,不要新建修改版。 +乱码需要清理干净,如: +--- + +## 二、核心格式修复(重点解决高频错误) + +### 2.1 列表格式修复 +- **修正列表层级错位**:核心解决主项与子项无缩进的问题(如主项* 标题,其子项需空两格后加*,而非与主项平齐),确保父子列表层级语法、视觉均清晰,比如「多人地图 / 单人战役」这类主项的子说明,必须作为子项缩进展示,杜绝所有子项与主项同级的错误; +- **修正无序列表基础错误**:规范无序列表的使用逻辑,保留原文列表意图,杜绝*被错误删除、替换、乱序的情况; +- **修正有序列表错误**:确保有序列表使用数字+点的格式(如1. 内容),避免使用错误的标记。 + +### 2.2 文本格式修复 +- **修正加粗符号错误**:确保加粗内容为**内容**正确渲染格式,杜绝**内容**转义符未生效的错误,无加粗符号缺失、多余情况; +- **修正斜体标记错误**:确保斜体内容为*内容*正确渲染格式,无斜体符号缺失、多余情况; +- **修正删除线格式**:确保删除线内容为~~内容~~正确渲染格式。 + +### 2.3 表格格式修复 +- 修正表格格式错误:完整保留表格原有结构,杜绝表格被分散展开、行列错乱、分隔符缺失的问题,确保表格语法完整、渲染正常; +- 调整表格列宽与对齐方式,确保表格美观易读。 + +### 2.4 代码块格式修复 +- 确保代码块使用```语言```和```的格式,正确标注代码语言; +- 修正未闭合的代码块,确保代码块独立成段。 + +### 2.5 链接格式修复 +- 确保链接使用[锚文本](URL)的格式,无未闭合的链接; +- 长链接做合理处理,避免影响文档排版。 + +### 2.6 数学公式格式修复 +- 确保行内公式使用$公式$的格式; +- 有些公式是块级公式却误用了行内公式格式,需修正为$$公式$$的格式,且$$符号需要单独占一行; +- 修正公式中的转义符错误,确保公式正确渲染。 + +### 2.7 基础语法兜底 +- 同步修正错位的标题层级、未闭合的链接/代码块、乱码的特殊符号、错误的斜体标记,确保所有Markdown语法准确生效,无失效或错误格式。 + +--- + +## 三、结构整理 + +### 3.1 段落划分 +- 梳理各章节内文本逻辑脉络,按内容模块划分段落(段落间空一行); +- 确保每个段落中心思想明确,内容连贯。 + +### 3.2 列表规范 +- 统一有序/无序列表的缩进规范,确保列表层级清晰; +- 列表项内容过长时,确保换行后缩进对齐。 + +### 3.3 标题层级 +- 统一标题层级:一级# 文档标题和主章节、二级 ## 大模块/子章节、三级 ### 子分类,不越级使用; +- 标题与正文间保留合理间距,章节标题样式统一。 + +--- + +## 四、视觉美化 + +### 4.1 标题美化 +- 标题:按「一级# 文档标题和主章节、二级 ## 大模块/子章节、三级 ### 子分类」分层,不越级使用,标题与正文间保留合理间距,章节标题样式统一; +- 标题前后添加适当的空行,增强视觉层次感。 + +### 4.2 文本美化 +- 重点:关键信息用粗体标注(避免大面积粗体),辅助说明用斜体,禁用过多花哨格式; +- 避免一行文本过长,适当控制行宽,提高可读性。 + +### 4.3 分隔美化 +- 不同章节间、章节内不同模块间用---分隔线清晰区隔,避免内容堆砌; +- 分隔线前后添加适当的空行,增强视觉效果。 + +### 4.4 图片美化 +- 图片格式不要在方括号中加任何东西,直接![]即可; +- 图片添加简洁说明且居中(若支持),图片前后添加适当的空行。 + +### 4.5 代码块美化 +- 代码块独立成段并标注对应语言,代码块前后添加适当的空行; +- 确保代码块格式正确,缩进统一。 + +--- + +## 五、细节优化 + +### 5.1 空格规范 +- 统一中英文/数字与标点间的空格,确保格式一致; +- 避免多余的空格和制表符。 + +### 5.2 文本优化 +- 修正各章节内错别字与不通顺语句,确保文本流畅; +- 删除冗余空行、重复内容和无意义符号,文本整洁紧凑。 + +### 5.3 特殊符号处理 +- 修正乱码的特殊符号,确保特殊符号正确显示; +- 避免使用过多的特殊符号,影响文档可读性。 + +--- + +## 六、核心原则 + +### 6.1 保留原义 +- 所有修改均基于原文核心内容,不增删、不篡改各章节原文,仅优化格式与排版!!!这个是最重要的 + +### 6.2 一致性原则 +- 跨章节格式保持完全统一,确保文档整体风格一致; +- 相同类型的内容使用相同的格式处理。 + +### 6.3 可读性优先 +- 所有修改以提高文档可读性为首要目标; +- 避免过度格式化,确保文档简洁明了。 + +--- + +## 七、输出要求 + +### 7.1 输出格式 +- 请按章节输出处理后的Markdown文本,每章节处理完成后标注该章节主要修改点,确保修复后的文本在任意Markdown编辑器中正常渲染,阅读体验更佳。 + +### 7.2 图片格式 +- 图片格式不要在方括号中加任何东西,直接![]即可。整理格式,直接给我。 + +### 7.3 检查清单 +- 整体:划分清晰章节,使用一级、二级、三级标题分层,不越级,符合Markdown标题规范; +- 任务拆分章节:拆分独立段落,修正段落间距,使核心要求(按章节处理、不新建版本)清晰突出; +- 核心格式修复章节:将原有零散要求整理为无序列表,规范列表缩进(无层级错位),修正语句间距,确保每类错误修复要求独立明确; +- 结构整理/细节优化/核心原则/输出要求章节:拆分独立段落,删除冗余空格,统一段落间距,关键提示(保留原义)突出; +- 视觉美化章节:将原有零散美化要求整理为无序列表,规范缩进,明确每类美化标准; +- 全局:添加合理分隔线(章节间),统一段落间距,修正语句排版,确保无加粗错误、无格式错乱,保留原文所有核心要求,未增删任何内容。 + +--- + +## 十、总结 + +Markdown文档的整理和美化是一个需要细心和耐心的工作,遵循以上规范和建议,可以确保文档格式正确、结构清晰、视觉美观,提高文档的可读性和专业度。在处理过程中,始终牢记核心原则:保留原义,仅优化格式与排版。 diff --git a/command/简化项目.md b/command/简化项目.md new file mode 100644 index 0000000..db1143f --- /dev/null +++ b/command/简化项目.md @@ -0,0 +1,19 @@ +# 代码审查与冗余问题优化任务要求 +对整个项目进行系统性代码审查,识别并定位一处对项目简洁性具有决定性影响的关键冗余问题。该问题可涵盖逻辑冗余、代码冗余、架构冗余或文件夹结构以及文件名不合理中的任意一种,但需满足以下核心条件: +- 必须是单一问题点; +- 必须是最最最严重的冗余问题,小问题可以忽略; +- 对当前代码库简洁性造成**显著负面影响**。 +- 每次执行前先制定计划,等我审核通过之后再执行。 + +## 需提交的具体修改建议包含以下维度 +1. 问题定位:明确冗余问题的具体位置、表现形式及核心特征; +2. 重构方案:针对该冗余问题提出可落地的代码/架构重构方案; +3. 预期收益:说明重构后在可读性、可维护性、架构清晰度等维度的具体提升效果; +4. 实施步骤:拆解重构的具体执行步骤,确保方案可落地。 + +### 核心目标 +修改后需显著提升代码的可读性、可维护性和整体架构清晰度。 + +注意:完成修复之后,针对涉及到的功能,需要给我验收测试方案,我来检查验收,看功能是否收到影响。 + +注意的注意:最最最关键的是不能影响原有的功能,不能产生bug!!! \ No newline at end of file diff --git a/dist-api/server.js b/dist-api/server.js new file mode 100644 index 0000000..a9e3467 --- /dev/null +++ b/dist-api/server.js @@ -0,0 +1,3026 @@ +var __glob = (map) => (path21) => { + var fn = map[path21]; + if (fn) return fn(); + throw new Error("Module not found in bundle: " + path21); +}; + +// api/app.ts +import express15 from "express"; +import cors from "cors"; +import dotenv from "dotenv"; + +// api/core/files/routes.ts +import express from "express"; +import fs from "fs/promises"; +import path3 from "path"; + +// api/utils/asyncHandler.ts +var asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; + +// api/utils/response.ts +var successResponse = (res, data, statusCode = 200) => { + const response = { + success: true, + data + }; + res.status(statusCode).json(response); +}; + +// api/utils/pathSafety.ts +import path2 from "path"; + +// api/config/index.ts +import path from "path"; +import { fileURLToPath } from "url"; +import os from "os"; +var __filename = fileURLToPath(import.meta.url); +var __dirname = path.dirname(__filename); +var config = { + get projectRoot() { + if (__dirname.includes("app.asar")) { + return path.resolve(__dirname, "..").replace("app.asar", "app.asar.unpacked"); + } + return path.resolve(__dirname, "../../"); + }, + get notebookRoot() { + return process.env.NOTEBOOK_ROOT ? path.resolve(process.env.NOTEBOOK_ROOT) : path.join(this.projectRoot, "notebook"); + }, + get tempRoot() { + return path.join(os.tmpdir(), "xcnote_uploads"); + }, + get serverPort() { + return parseInt(process.env.PORT || "3001", 10); + }, + get isVercel() { + return !!process.env.VERCEL; + }, + get isElectron() { + return __dirname.includes("app.asar"); + }, + get isDev() { + return !this.isElectron && !this.isVercel; + } +}; +var PATHS = { + get PROJECT_ROOT() { + return config.projectRoot; + }, + get NOTEBOOK_ROOT() { + return config.notebookRoot; + }, + get TEMP_ROOT() { + return config.tempRoot; + } +}; + +// api/config/paths.ts +var PROJECT_ROOT = PATHS.PROJECT_ROOT; +var NOTEBOOK_ROOT = PATHS.NOTEBOOK_ROOT; +var TEMP_ROOT = PATHS.TEMP_ROOT; + +// shared/errors/index.ts +var AppError = class extends Error { + constructor(code, message, statusCode = 500, details) { + super(message); + this.code = code; + this.name = "AppError"; + this.statusCode = statusCode; + this.details = details; + } + statusCode; + details; + toJSON() { + return { + name: this.name, + code: this.code, + message: this.message, + statusCode: this.statusCode, + details: this.details + }; + } +}; +var ValidationError = class extends AppError { + constructor(message, details) { + super("VALIDATION_ERROR", message, 400, details); + this.name = "ValidationError"; + } +}; +var NotFoundError = class extends AppError { + constructor(message = "Resource not found", details) { + super("NOT_FOUND", message, 404, details); + this.name = "NotFoundError"; + } +}; +var AccessDeniedError = class extends AppError { + constructor(message = "Access denied", details) { + super("ACCESS_DENIED", message, 403, details); + this.name = "AccessDeniedError"; + } +}; +var BadRequestError = class extends AppError { + constructor(message, details) { + super("BAD_REQUEST", message, 400, details); + this.name = "BadRequestError"; + } +}; +var NotADirectoryError = class extends AppError { + constructor(message = "\u4E0D\u662F\u76EE\u5F55", details) { + super("NOT_A_DIRECTORY", message, 400, details); + this.name = "NotADirectoryError"; + } +}; +var AlreadyExistsError = class extends AppError { + constructor(message = "Resource already exists", details) { + super("ALREADY_EXISTS", message, 409, details); + this.name = "AlreadyExistsError"; + } +}; +var ForbiddenError = class extends AppError { + constructor(message = "\u7981\u6B62\u8BBF\u95EE", details) { + super("FORBIDDEN", message, 403, details); + this.name = "ForbiddenError"; + } +}; +var UnsupportedMediaTypeError = class extends AppError { + constructor(message = "\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B", details) { + super("UNSUPPORTED_MEDIA_TYPE", message, 415, details); + this.name = "UnsupportedMediaTypeError"; + } +}; +var ResourceLockedError = class extends AppError { + constructor(message = "\u8D44\u6E90\u5DF2\u9501\u5B9A", details) { + super("RESOURCE_LOCKED", message, 423, details); + this.name = "ResourceLockedError"; + } +}; +var InternalError = class extends AppError { + constructor(message = "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF", details) { + super("INTERNAL_ERROR", message, 500, details); + this.name = "InternalError"; + } +}; +function isAppError(error) { + return error instanceof AppError; +} +function isNodeError(error) { + return error instanceof Error && "code" in error; +} + +// api/utils/pathSafety.ts +var DANGEROUS_PATTERNS = [ + /\.\./, + /\0/, + /%2e%2e[%/]/i, + /%252e%252e[%/]/i, + /\.\.%2f/i, + /\.\.%5c/i, + /%c0%ae/i, + /%c1%9c/i, + /%c0%ae%c0%ae/i, + /%c1%9c%c1%9c/i, + /\.\.%c0%af/i, + /\.\.%c1%9c/i, + /%252e/i, + /%uff0e/i, + /%u002e/i +]; +var DOUBLE_ENCODE_PATTERNS = [ + /%25[0-9a-fA-F]{2}/, + /%[0-9a-fA-F]{2}%[0-9a-fA-F]{2}/ +]; +var normalizeRelPath = (input) => { + const trimmed = input.replace(/\0/g, "").trim(); + return trimmed.replace(/^[/\\]+/, ""); +}; +var containsPathTraversal = (input) => { + const decoded = decodeURIComponentSafe(input); + return DANGEROUS_PATTERNS.some((pattern) => pattern.test(input) || pattern.test(decoded)); +}; +var containsDoubleEncoding = (input) => { + return DOUBLE_ENCODE_PATTERNS.some((pattern) => pattern.test(input)); +}; +var hasPathSecurityIssues = (input) => { + return containsPathTraversal(input) || containsDoubleEncoding(input); +}; +var decodeURIComponentSafe = (input) => { + try { + return decodeURIComponent(input); + } catch { + return input; + } +}; +var resolveNotebookPath = (relPath) => { + if (hasPathSecurityIssues(relPath)) { + throw new AccessDeniedError("Path traversal detected"); + } + const safeRelPath = normalizeRelPath(relPath); + const notebookRoot = path2.resolve(NOTEBOOK_ROOT); + const fullPath = path2.resolve(notebookRoot, safeRelPath); + if (!fullPath.startsWith(notebookRoot)) { + throw new AccessDeniedError("Access denied"); + } + return { safeRelPath, fullPath }; +}; + +// shared/utils/path.ts +var toPosixPath = (p) => p.replace(/\\/g, "/"); + +// shared/utils/date.ts +var pad2 = (n) => String(n).padStart(2, "0"); +var pad3 = (n) => String(n).padStart(3, "0"); +var formatTimestamp = (d) => { + const yyyy = d.getFullYear(); + const mm = pad2(d.getMonth() + 1); + const dd = pad2(d.getDate()); + const hh = pad2(d.getHours()); + const mi = pad2(d.getMinutes()); + const ss = pad2(d.getSeconds()); + const ms = pad3(d.getMilliseconds()); + return `${yyyy}${mm}${dd}_${hh}${mi}${ss}_${ms}`; +}; + +// api/middlewares/validate.ts +import { ZodError } from "zod"; +var validateBody = (schema) => { + return (req, _res, next) => { + try { + req.body = schema.parse(req.body); + next(); + } catch (error) { + if (error instanceof ZodError) { + next(new ValidationError("Request validation failed", { issues: error.issues })); + } else { + next(error); + } + } + }; +}; +var validateQuery = (schema) => { + return (req, _res, next) => { + try { + req.query = schema.parse(req.query); + next(); + } catch (error) { + if (error instanceof ZodError) { + next(new ValidationError("Query validation failed", { issues: error.issues })); + } else { + next(error); + } + } + }; +}; + +// api/schemas/files.ts +import { z } from "zod"; +var listFilesQuerySchema = z.object({ + path: z.string().optional().default("") +}); +var contentQuerySchema = z.object({ + path: z.string().min(1) +}); +var rawQuerySchema = z.object({ + path: z.string().min(1) +}); +var pathSchema = z.object({ + path: z.string().min(1) +}); +var saveFileSchema = z.object({ + path: z.string().min(1), + content: z.string() +}); +var renameSchema = z.object({ + oldPath: z.string().min(1), + newPath: z.string().min(1) +}); +var searchSchema = z.object({ + keywords: z.array(z.string()).min(1) +}); +var existsSchema = z.object({ + path: z.string().min(1) +}); +var createDirSchema = z.object({ + path: z.string().min(1) +}); +var createFileSchema = z.object({ + path: z.string().min(1) +}); + +// api/schemas/pydemos.ts +import { z as z2 } from "zod"; +var listPyDemosQuerySchema = z2.object({ + year: z2.string().optional() +}); +var createPyDemoSchema = z2.object({ + name: z2.string().min(1), + year: z2.string().min(1), + month: z2.string().min(1), + folderStructure: z2.string().optional() +}); +var deletePyDemoSchema = z2.object({ + path: z2.string().min(1) +}); +var renamePyDemoSchema = z2.object({ + oldPath: z2.string().min(1), + newName: z2.string().min(1) +}); + +// api/utils/logger.ts +var createLogger = () => { + const isProd = process.env.NODE_ENV === "production"; + const debug = isProd ? () => { + } : console.debug.bind(console); + const info = console.info.bind(console); + const warn = console.warn.bind(console); + const error = console.error.bind(console); + return { debug, info, warn, error }; +}; +var logger = createLogger(); + +// api/core/files/routes.ts +var router = express.Router(); +router.get( + "/", + validateQuery(listFilesQuerySchema), + asyncHandler(async (req, res) => { + const relPath = req.query.path; + const { safeRelPath, fullPath } = resolveNotebookPath(relPath); + try { + await fs.access(fullPath); + } catch { + throw new NotFoundError("\u8DEF\u5F84\u4E0D\u5B58\u5728"); + } + const stats = await fs.stat(fullPath); + if (!stats.isDirectory()) { + throw new NotADirectoryError(); + } + const files = await fs.readdir(fullPath); + const items = await Promise.all( + files.map(async (name) => { + const filePath = path3.join(fullPath, name); + try { + const fileStats = await fs.stat(filePath); + return { + name, + type: fileStats.isDirectory() ? "dir" : "file", + size: fileStats.size, + modified: fileStats.mtime.toISOString(), + path: toPosixPath(path3.join(safeRelPath, name)) + }; + } catch { + return null; + } + }) + ); + const visibleItems = items.filter((i) => i !== null && !i.name.startsWith(".")); + visibleItems.sort((a, b) => { + if (a.type === b.type) return a.name.localeCompare(b.name); + return a.type === "dir" ? -1 : 1; + }); + successResponse(res, { items: visibleItems }); + }) +); +router.get( + "/content", + validateQuery(contentQuerySchema), + asyncHandler(async (req, res) => { + const relPath = req.query.path; + const { fullPath } = resolveNotebookPath(relPath); + const stats = await fs.stat(fullPath).catch(() => { + throw new NotFoundError("\u6587\u4EF6\u4E0D\u5B58\u5728"); + }); + if (!stats.isFile()) throw new BadRequestError("\u4E0D\u662F\u6587\u4EF6"); + const content = await fs.readFile(fullPath, "utf-8"); + successResponse(res, { + content, + metadata: { + size: stats.size, + modified: stats.mtime.toISOString() + } + }); + }) +); +router.get( + "/raw", + validateQuery(rawQuerySchema), + asyncHandler(async (req, res) => { + const relPath = req.query.path; + const { fullPath } = resolveNotebookPath(relPath); + const stats = await fs.stat(fullPath).catch(() => { + throw new NotFoundError("\u6587\u4EF6\u4E0D\u5B58\u5728"); + }); + if (!stats.isFile()) throw new BadRequestError("\u4E0D\u662F\u6587\u4EF6"); + const ext = path3.extname(fullPath).toLowerCase(); + const mimeTypes = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".pdf": "application/pdf" + }; + const mimeType = mimeTypes[ext]; + if (mimeType) res.setHeader("Content-Type", mimeType); + res.sendFile(fullPath); + }) +); +router.post( + "/save", + validateBody(saveFileSchema), + asyncHandler(async (req, res) => { + const { path: relPath, content } = req.body; + const { fullPath } = resolveNotebookPath(relPath); + await fs.mkdir(path3.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content, "utf-8"); + successResponse(res, null); + }) +); +router.delete( + "/delete", + validateBody(pathSchema), + asyncHandler(async (req, res) => { + const { path: relPath } = req.body; + const { fullPath } = resolveNotebookPath(relPath); + await fs.stat(fullPath).catch(() => { + throw new NotFoundError("\u6587\u4EF6\u6216\u76EE\u5F55\u4E0D\u5B58\u5728"); + }); + const { fullPath: rbDir } = resolveNotebookPath("RB"); + await fs.mkdir(rbDir, { recursive: true }); + const originalName = path3.basename(fullPath); + const now = /* @__PURE__ */ new Date(); + const year = now.getFullYear(); + const month = pad2(now.getMonth() + 1); + const day = pad2(now.getDate()); + const timestamp = `${year}${month}${day}`; + const newName = `${timestamp}_${originalName}`; + const rbDestPath = path3.join(rbDir, newName); + await fs.rename(fullPath, rbDestPath); + successResponse(res, null); + }) +); +router.post( + "/exists", + validateBody(pathSchema), + asyncHandler(async (req, res) => { + const { path: relPath } = req.body; + const { fullPath } = resolveNotebookPath(relPath); + try { + const stats = await fs.stat(fullPath); + const type = stats.isDirectory() ? "dir" : stats.isFile() ? "file" : null; + successResponse(res, { exists: true, type }); + } catch (err) { + if (isNodeError(err) && err.code === "ENOENT") { + successResponse(res, { exists: false, type: null }); + return; + } + throw err; + } + }) +); +router.post( + "/create/dir", + validateBody(createDirSchema), + asyncHandler(async (req, res) => { + const { path: relPath } = req.body; + const { fullPath } = resolveNotebookPath(relPath); + try { + await fs.mkdir(fullPath, { recursive: true }); + } catch (err) { + if (isNodeError(err)) { + if (err.code === "EEXIST") { + throw new AlreadyExistsError("\u8DEF\u5F84\u5DF2\u5B58\u5728"); + } + if (err.code === "EACCES") { + throw new ForbiddenError("\u6CA1\u6709\u6743\u9650\u521B\u5EFA\u76EE\u5F55"); + } + } + throw err; + } + successResponse(res, null); + }) +); +router.post( + "/create/file", + validateBody(createFileSchema), + asyncHandler(async (req, res) => { + const { path: relPath } = req.body; + const { fullPath } = resolveNotebookPath(relPath); + await fs.mkdir(path3.dirname(fullPath), { recursive: true }); + try { + const fileName = path3.basename(relPath, ".md"); + const content = `# ${fileName}`; + await fs.writeFile(fullPath, content, { encoding: "utf-8", flag: "wx" }); + } catch (err) { + if (isNodeError(err)) { + if (err.code === "EEXIST") throw new AlreadyExistsError("\u8DEF\u5F84\u5DF2\u5B58\u5728"); + if (err.code === "EACCES") throw new ForbiddenError("\u6CA1\u6709\u6743\u9650\u521B\u5EFA\u6587\u4EF6"); + } + throw err; + } + successResponse(res, null); + }) +); +router.post( + "/rename", + validateBody(renameSchema), + asyncHandler(async (req, res) => { + const { oldPath, newPath } = req.body; + const { fullPath: oldFullPath } = resolveNotebookPath(oldPath); + const { fullPath: newFullPath } = resolveNotebookPath(newPath); + await fs.mkdir(path3.dirname(newFullPath), { recursive: true }); + try { + await fs.rename(oldFullPath, newFullPath); + } catch (err) { + if (isNodeError(err)) { + if (err.code === "ENOENT") { + throw new NotFoundError("\u6587\u4EF6\u4E0D\u5B58\u5728"); + } + if (err.code === "EEXIST") { + throw new AlreadyExistsError("\u8DEF\u5F84\u5DF2\u5B58\u5728"); + } + if (err.code === "EPERM" || err.code === "EACCES") { + throw new ForbiddenError("\u6CA1\u6709\u6743\u9650\u91CD\u547D\u540D\u6587\u4EF6\u6216\u76EE\u5F55"); + } + if (err.code === "EBUSY") { + throw new ResourceLockedError("\u6587\u4EF6\u6216\u76EE\u5F55\u6B63\u5728\u4F7F\u7528\u4E2D\u6216\u88AB\u9501\u5B9A"); + } + } + logger.error("\u91CD\u547D\u540D\u9519\u8BEF:", err); + throw new InternalError("\u91CD\u547D\u540D\u6587\u4EF6\u6216\u76EE\u5F55\u5931\u8D25"); + } + successResponse(res, null); + }) +); +var routes_default = router; + +// api/core/events/routes.ts +import express2 from "express"; + +// api/events/eventBus.ts +var clients = []; +var eventBus = { + addClient: (res) => { + clients.push(res); + logger.info(`SSE client connected. Total clients: ${clients.length}`); + }, + removeClient: (res) => { + clients = clients.filter((c) => c !== res); + logger.info(`SSE client disconnected. Total clients: ${clients.length}`); + }, + broadcast: (payload) => { + const data = `data: ${JSON.stringify(payload)} + +`; + logger.info(`Broadcasting to ${clients.length} clients: ${payload.event} - ${payload.path || ""}`); + clients = clients.filter((client) => { + try { + client.write(data); + return true; + } catch (error) { + logger.warn("SSE client write failed, removing"); + return false; + } + }); + } +}; + +// api/core/events/routes.ts +var router2 = express2.Router(); +router2.get("/", (req, res) => { + if (process.env.VERCEL) { + const response = { + success: false, + error: { code: "SSE_UNSUPPORTED", message: "SSE\u5728\u65E0\u670D\u52A1\u5668\u8FD0\u884C\u65F6\u4E2D\u4E0D\u53D7\u652F\u6301" } + }; + return res.status(501).json(response); + } + const headers = { + "Content-Type": "text/event-stream", + "Connection": "keep-alive", + "Cache-Control": "no-cache" + }; + res.writeHead(200, headers); + res.write(`data: ${JSON.stringify({ event: "connected" })} + +`); + eventBus.addClient(res); + req.on("close", () => { + eventBus.removeClient(res); + }); +}); +var routes_default2 = router2; + +// api/core/settings/routes.ts +import express3 from "express"; +import fs2 from "fs/promises"; +import path4 from "path"; +var router3 = express3.Router(); +var getSettingsPath = () => path4.join(NOTEBOOK_ROOT, ".config", "settings.json"); +router3.get( + "/", + asyncHandler(async (req, res) => { + const settingsPath = getSettingsPath(); + try { + const content = await fs2.readFile(settingsPath, "utf-8"); + const settings = JSON.parse(content); + successResponse(res, settings); + } catch (error) { + successResponse(res, {}); + } + }) +); +router3.post( + "/", + asyncHandler(async (req, res) => { + const settings = req.body; + const settingsPath = getSettingsPath(); + const configDir = path4.dirname(settingsPath); + try { + await fs2.mkdir(configDir, { recursive: true }); + let existingSettings = {}; + try { + const content = await fs2.readFile(settingsPath, "utf-8"); + existingSettings = JSON.parse(content); + } catch { + } + const newSettings = { ...existingSettings, ...settings }; + await fs2.writeFile(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8"); + successResponse(res, newSettings); + } catch (error) { + throw error; + } + }) +); +var routes_default3 = router3; + +// api/core/upload/routes.ts +import express4 from "express"; +import fs4 from "fs/promises"; +import path6 from "path"; + +// api/utils/file.ts +import fs3 from "fs/promises"; +import path5 from "path"; +var getUniqueFilename = async (imagesDirFullPath, baseName, ext) => { + const maxAttempts = 1e3; + for (let i = 0; i < maxAttempts; i++) { + const suffix = i === 0 ? "" : `-${i + 1}`; + const filename = `${baseName}${suffix}${ext}`; + const fullPath = path5.join(imagesDirFullPath, filename); + try { + await fs3.access(fullPath); + } catch { + return filename; + } + } + throw new InternalError("Failed to generate unique filename"); +}; +var mimeToExt = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/jpg": ".jpg", + "image/gif": ".gif", + "image/webp": ".webp" +}; +var IMAGE_MAGIC_BYTES = { + "image/png": { bytes: [137, 80, 78, 71, 13, 10, 26, 10] }, + "image/jpeg": { bytes: [255, 216, 255] }, + "image/gif": { bytes: [71, 73, 70, 56] }, + "image/webp": { bytes: [82, 73, 70, 70], offset: 0 } +}; +var WEBP_WEBP_MARKER = [87, 69, 66, 80]; +var MIN_IMAGE_SIZE = 16; +var MAX_IMAGE_SIZE = 8 * 1024 * 1024; +var validateImageBuffer = (buffer, claimedMimeType) => { + if (buffer.byteLength < MIN_IMAGE_SIZE) { + throw new ValidationError("Image file is too small or corrupted"); + } + if (buffer.byteLength > MAX_IMAGE_SIZE) { + throw new ValidationError("Image file is too large"); + } + const magicInfo = IMAGE_MAGIC_BYTES[claimedMimeType]; + if (!magicInfo) { + throw new ValidationError("Unsupported image type for content validation"); + } + const offset = magicInfo.offset || 0; + const expectedBytes = magicInfo.bytes; + for (let i = 0; i < expectedBytes.length; i++) { + if (buffer[offset + i] !== expectedBytes[i]) { + throw new ValidationError("Image content does not match the claimed file type"); + } + } + if (claimedMimeType === "image/webp") { + if (buffer.byteLength < 12) { + throw new ValidationError("WebP image is corrupted"); + } + for (let i = 0; i < WEBP_WEBP_MARKER.length; i++) { + if (buffer[8 + i] !== WEBP_WEBP_MARKER[i]) { + throw new ValidationError("WebP image content is invalid"); + } + } + } +}; +var detectImageMimeType = (buffer) => { + if (buffer.byteLength < 8) return null; + if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10) { + return "image/png"; + } + if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) { + return "image/jpeg"; + } + if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 56) { + return "image/gif"; + } + if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) { + return "image/webp"; + } + return null; +}; + +// api/core/upload/routes.ts +var router4 = express4.Router(); +var parseImageDataUrl = (dataUrl) => { + const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/); + if (!match) return null; + const [, mimeType, base64Data] = match; + return { mimeType, base64Data: base64Data.replace(/\s/g, "") }; +}; +router4.post( + "/image", + asyncHandler(async (req, res) => { + const { image } = req.body; + if (!image) throw new ValidationError("\u9700\u8981\u56FE\u7247\u6570\u636E"); + const parsed = parseImageDataUrl(image); + if (!parsed) { + throw new ValidationError("\u65E0\u6548\u7684\u56FE\u7247\u6570\u636EURL"); + } + const ext = mimeToExt[parsed.mimeType]; + if (!ext) { + throw new UnsupportedMediaTypeError("\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B"); + } + const buffer = Buffer.from(parsed.base64Data, "base64"); + validateImageBuffer(buffer, parsed.mimeType); + const detectedMimeType = detectImageMimeType(buffer); + if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { + throw new ValidationError("\u56FE\u7247\u5185\u5BB9\u7C7B\u578B\u4E0D\u5339\u914D\u6216\u56FE\u7247\u5DF2\u635F\u574F"); + } + const now = /* @__PURE__ */ new Date(); + const year = now.getFullYear(); + const month = pad2(now.getMonth() + 1); + const day = pad2(now.getDate()); + const imagesSubDir = `images/${year}/${month}/${day}`; + const { fullPath: imagesDirFullPath } = resolveNotebookPath(imagesSubDir); + await fs4.mkdir(imagesDirFullPath, { recursive: true }); + const baseName = formatTimestamp(now); + const filename = await getUniqueFilename(imagesDirFullPath, baseName, ext); + const relPath = `${imagesSubDir}/${filename}`; + const { fullPath } = resolveNotebookPath(relPath); + await fs4.writeFile(fullPath, buffer); + successResponse(res, { name: toPosixPath(relPath), path: toPosixPath(relPath) }); + }) +); +router4.post( + "/wallpaper", + asyncHandler(async (req, res) => { + const { image } = req.body; + if (!image) throw new ValidationError("\u9700\u8981\u56FE\u7247\u6570\u636E"); + const parsed = parseImageDataUrl(image); + if (!parsed) { + throw new ValidationError("\u65E0\u6548\u7684\u56FE\u7247\u6570\u636EURL"); + } + const allowedWallpaperTypes = ["image/png", "image/jpeg", "image/webp"]; + if (!allowedWallpaperTypes.includes(parsed.mimeType)) { + throw new UnsupportedMediaTypeError("\u58C1\u7EB8\u53EA\u652F\u6301PNG\u3001JPEG\u548CWebP\u683C\u5F0F"); + } + const buffer = Buffer.from(parsed.base64Data, "base64"); + validateImageBuffer(buffer, parsed.mimeType); + const detectedMimeType = detectImageMimeType(buffer); + if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { + throw new ValidationError("\u56FE\u7247\u5185\u5BB9\u7C7B\u578B\u4E0D\u5339\u914D\u6216\u56FE\u7247\u5DF2\u635F\u574F"); + } + const configDir = path6.join(NOTEBOOK_ROOT, ".config"); + const backgroundPath = path6.join(configDir, "background.png"); + await fs4.mkdir(configDir, { recursive: true }); + await fs4.writeFile(backgroundPath, buffer); + successResponse(res, { message: "\u58C1\u7EB8\u5DF2\u66F4\u65B0" }); + }) +); +var routes_default4 = router4; + +// api/core/search/routes.ts +import express5 from "express"; +import fs5 from "fs/promises"; +import path7 from "path"; +var router5 = express5.Router(); +router5.post( + "/", + asyncHandler(async (req, res) => { + const { keywords } = req.body; + if (!keywords || !Array.isArray(keywords) || keywords.length === 0) { + successResponse(res, { items: [] }); + return; + } + const searchTerms = keywords.map((k) => k.trim().toLowerCase()).filter((k) => k.length > 0); + if (searchTerms.length === 0) { + successResponse(res, { items: [] }); + return; + } + const { fullPath: rootPath } = resolveNotebookPath(""); + const results = []; + const maxResults = 100; + const searchDir = async (dir, relativeDir) => { + if (results.length >= maxResults) return; + const entries = await fs5.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (results.length >= maxResults) break; + const entryPath = path7.join(dir, entry.name); + const entryRelativePath = path7.join(relativeDir, entry.name); + if (entry.name.startsWith(".") || entry.name === "RB" || entry.name === "node_modules") continue; + if (entry.isDirectory()) { + await searchDir(entryPath, entryRelativePath); + } else if (entry.isFile()) { + const fileNameLower = entry.name.toLowerCase(); + let contentLower = ""; + let contentLoaded = false; + const checkKeyword = async (term) => { + if (fileNameLower.includes(term)) return true; + if (entry.name.toLowerCase().endsWith(".md")) { + if (!contentLoaded) { + try { + const content = await fs5.readFile(entryPath, "utf-8"); + contentLower = content.toLowerCase(); + contentLoaded = true; + } catch { + return false; + } + } + return contentLower.includes(term); + } + return false; + }; + let allMatched = true; + for (const term of searchTerms) { + const matched = await checkKeyword(term); + if (!matched) { + allMatched = false; + break; + } + } + if (allMatched) { + results.push({ + name: entry.name, + path: toPosixPath(entryRelativePath), + type: "file", + size: 0, + modified: (/* @__PURE__ */ new Date()).toISOString() + }); + } + } + } + }; + await searchDir(rootPath, ""); + successResponse(res, { items: results, limited: results.length >= maxResults }); + }) +); +var routes_default5 = router5; + +// shared/constants/errors.ts +var ERROR_CODES = { + PATH_NOT_FOUND: "PATH_NOT_FOUND", + NOT_A_DIRECTORY: "NOT_A_DIRECTORY", + ACCESS_DENIED: "ACCESS_DENIED", + FILE_EXISTS: "FILE_EXISTS", + INVALID_PATH: "INVALID_PATH", + VALIDATION_ERROR: "VALIDATION_ERROR", + INTERNAL_ERROR: "INTERNAL_ERROR", + NOT_FOUND: "NOT_FOUND", + BAD_REQUEST: "BAD_REQUEST", + NAME_GENERATION_FAILED: "NAME_GENERATION_FAILED", + SSE_UNSUPPORTED: "SSE_UNSUPPORTED", + ALREADY_EXISTS: "ALREADY_EXISTS", + NOT_A_FILE: "NOT_A_FILE", + FORBIDDEN: "FORBIDDEN", + UNSUPPORTED_MEDIA_TYPE: "UNSUPPORTED_MEDIA_TYPE", + PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE", + RESOURCE_LOCKED: "RESOURCE_LOCKED", + INVALID_NAME: "INVALID_NAME" +}; + +// api/middlewares/errorHandler.ts +var errorHandler = (err, _req, res, _next) => { + let statusCode = 500; + let code = ERROR_CODES.INTERNAL_ERROR; + let message = "Server internal error"; + let details = void 0; + if (isAppError(err)) { + statusCode = err.statusCode; + code = err.code; + message = err.message; + details = err.details; + } else if (isNodeError(err)) { + message = err.message; + if (process.env.NODE_ENV !== "production") { + details = { stack: err.stack, nodeErrorCode: err.code }; + } + } else if (err instanceof Error) { + message = err.message; + if (process.env.NODE_ENV !== "production") { + details = { stack: err.stack }; + } + } + logger.error(err); + const response = { + success: false, + error: { + code, + message, + details: process.env.NODE_ENV === "production" ? void 0 : details + } + }; + res.status(statusCode).json(response); +}; + +// api/infra/moduleManager.ts +var ModuleManager = class { + modules = /* @__PURE__ */ new Map(); + activeModules = /* @__PURE__ */ new Set(); + container; + constructor(container2) { + this.container = container2; + } + async register(module) { + const { id, dependencies = [] } = module.metadata; + for (const dep of dependencies) { + if (!this.modules.has(dep)) { + throw new Error(`Module '${id}' depends on '${dep}' which is not registered`); + } + } + this.modules.set(id, module); + if (module.lifecycle?.onLoad) { + await module.lifecycle.onLoad(this.container); + } + } + async activate(id) { + const module = this.modules.get(id); + if (!module) { + throw new Error(`Module '${id}' not found`); + } + if (this.activeModules.has(id)) { + return; + } + const { dependencies = [] } = module.metadata; + for (const dep of dependencies) { + await this.activate(dep); + } + if (module.lifecycle?.onActivate) { + await module.lifecycle.onActivate(this.container); + } + this.activeModules.add(id); + } + async deactivate(id) { + const module = this.modules.get(id); + if (!module) return; + if (!this.activeModules.has(id)) return; + if (module.lifecycle?.onDeactivate) { + await module.lifecycle.onDeactivate(this.container); + } + this.activeModules.delete(id); + } + getModule(id) { + return this.modules.get(id); + } + getAllModules() { + return Array.from(this.modules.values()).sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0)); + } + getActiveModules() { + return Array.from(this.activeModules); + } +}; + +// api/infra/container.ts +var ServiceContainer = class { + descriptors = /* @__PURE__ */ new Map(); + singletonInstances = /* @__PURE__ */ new Map(); + scopedInstances = /* @__PURE__ */ new Map(); + resolutionStack = []; + disposed = false; + register(nameOrDescriptor, factory) { + this.ensureNotDisposed(); + if (typeof nameOrDescriptor === "string") { + const descriptor = { + name: nameOrDescriptor, + factory, + lifetime: "singleton" /* Singleton */ + }; + this.descriptors.set(nameOrDescriptor, descriptor); + } else { + this.descriptors.set(nameOrDescriptor.name, nameOrDescriptor); + } + } + async get(name) { + this.ensureNotDisposed(); + return this.resolveInternal(name, null); + } + getSync(name) { + this.ensureNotDisposed(); + const descriptor = this.descriptors.get(name); + if (!descriptor) { + throw new Error(`Service '${name}' not registered`); + } + if (descriptor.lifetime === "singleton" /* Singleton */) { + if (this.singletonInstances.has(name)) { + return this.singletonInstances.get(name); + } + } + const result = descriptor.factory(); + if (result instanceof Promise) { + throw new Error( + `Service '${name}' has an async factory but getSync() was called. Use get() instead.` + ); + } + if (descriptor.lifetime === "singleton" /* Singleton */) { + this.singletonInstances.set(name, result); + } + return result; + } + createScope(scopeId) { + this.ensureNotDisposed(); + return new ServiceScope(this, scopeId); + } + has(name) { + return this.descriptors.has(name); + } + async dispose() { + if (this.disposed) { + return; + } + for (const [name, instance] of this.singletonInstances) { + const descriptor = this.descriptors.get(name); + if (descriptor?.onDispose) { + try { + await descriptor.onDispose(instance); + } catch (error) { + console.error(`Error disposing service '${name}':`, error); + } + } + } + for (const [, scopeMap] of this.scopedInstances) { + for (const [name, instance] of scopeMap) { + const descriptor = this.descriptors.get(name); + if (descriptor?.onDispose) { + try { + await descriptor.onDispose(instance); + } catch (error) { + console.error(`Error disposing scoped service '${name}':`, error); + } + } + } + } + this.singletonInstances.clear(); + this.scopedInstances.clear(); + this.descriptors.clear(); + this.resolutionStack = []; + this.disposed = true; + } + clear() { + this.singletonInstances.clear(); + this.scopedInstances.clear(); + this.descriptors.clear(); + this.resolutionStack = []; + } + isDisposed() { + return this.disposed; + } + async resolveInternal(name, scopeId) { + if (this.resolutionStack.includes(name)) { + const cycle = [...this.resolutionStack, name].join(" -> "); + throw new Error(`Circular dependency detected: ${cycle}`); + } + const descriptor = this.descriptors.get(name); + if (!descriptor) { + throw new Error(`Service '${name}' not registered`); + } + if (descriptor.lifetime === "singleton" /* Singleton */) { + if (this.singletonInstances.has(name)) { + return this.singletonInstances.get(name); + } + this.resolutionStack.push(name); + try { + const instance = await descriptor.factory(); + this.singletonInstances.set(name, instance); + return instance; + } finally { + this.resolutionStack.pop(); + } + } + if (descriptor.lifetime === "scoped" /* Scoped */) { + if (!scopeId) { + throw new Error( + `Scoped service '${name}' cannot be resolved outside of a scope. Use createScope() first.` + ); + } + let scopeMap = this.scopedInstances.get(scopeId); + if (!scopeMap) { + scopeMap = /* @__PURE__ */ new Map(); + this.scopedInstances.set(scopeId, scopeMap); + } + if (scopeMap.has(name)) { + return scopeMap.get(name); + } + this.resolutionStack.push(name); + try { + const instance = await descriptor.factory(); + scopeMap.set(name, instance); + return instance; + } finally { + this.resolutionStack.pop(); + } + } + this.resolutionStack.push(name); + try { + const instance = await descriptor.factory(); + return instance; + } finally { + this.resolutionStack.pop(); + } + } + ensureNotDisposed() { + if (this.disposed) { + throw new Error("ServiceContainer has been disposed"); + } + } +}; +var ServiceScope = class { + container; + scopeId; + disposed = false; + constructor(container2, scopeId) { + this.container = container2; + this.scopeId = scopeId; + } + async get(name) { + if (this.disposed) { + throw new Error("ServiceScope has been disposed"); + } + return this.container["resolveInternal"](name, this.scopeId); + } + async dispose() { + if (this.disposed) { + return; + } + const scopeMap = this.container["scopedInstances"].get(this.scopeId); + if (scopeMap) { + for (const [name, instance] of scopeMap) { + const descriptor = this.container["descriptors"].get(name); + if (descriptor?.onDispose) { + try { + await descriptor.onDispose(instance); + } catch (error) { + console.error(`Error disposing scoped service '${name}':`, error); + } + } + } + this.container["scopedInstances"].delete(this.scopeId); + } + this.disposed = true; + } + isDisposed() { + return this.disposed; + } +}; + +// api/modules/index.ts +import { readdirSync, statSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath as fileURLToPath3 } from "url"; + +// shared/modules/types.ts +function defineApiModule(config2) { + return config2; +} +function defineEndpoints(endpoints) { + return endpoints; +} + +// shared/modules/todo/api.ts +var TODO_ENDPOINTS = defineEndpoints({ + list: { path: "/", method: "GET" }, + save: { path: "/save", method: "POST" }, + add: { path: "/add", method: "POST" }, + toggle: { path: "/toggle", method: "POST" }, + update: { path: "/update", method: "POST" }, + delete: { path: "/delete", method: "DELETE" } +}); + +// shared/modules/todo/index.ts +var TODO_MODULE = defineApiModule({ + id: "todo", + name: "TODO", + basePath: "/todo", + order: 30, + version: "1.0.0", + endpoints: TODO_ENDPOINTS +}); + +// api/modules/todo/service.ts +import fs6 from "fs/promises"; +import path8 from "path"; + +// api/modules/todo/parser.ts +var parseTodoContent = (content) => { + const lines = content.split("\n"); + const result = []; + let currentDate = null; + let currentItems = []; + let itemId = 0; + for (const line of lines) { + const dateMatch = line.match(/^##\s*(\d{4}-\d{2}-\d{2})/); + if (dateMatch) { + if (currentDate) { + result.push({ date: currentDate, items: currentItems }); + } + currentDate = dateMatch[1]; + currentItems = []; + } else if (currentDate) { + const todoMatch = line.match(/^- (√|○) (.*)$/); + if (todoMatch) { + currentItems.push({ + id: `${currentDate}-${itemId++}`, + content: todoMatch[2], + completed: todoMatch[1] === "\u221A" + }); + } + } + } + if (currentDate) { + result.push({ date: currentDate, items: currentItems }); + } + return result; +}; +var generateTodoContent = (dayTodos) => { + const lines = []; + const sortedDays = [...dayTodos].sort((a, b) => a.date.localeCompare(b.date)); + for (const day of sortedDays) { + lines.push(`## ${day.date}`); + for (const item of day.items) { + const checkbox = item.completed ? "\u221A" : "\u25CB"; + lines.push(`- ${checkbox} ${item.content}`); + } + lines.push(""); + } + return lines.join("\n").trimEnd(); +}; + +// api/modules/todo/service.ts +var TodoService = class { + constructor(deps = {}) { + this.deps = deps; + } + getTodoFilePath(year, month) { + const yearStr = year.toString(); + const monthStr = month.toString().padStart(2, "0"); + const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md`; + const { fullPath } = resolveNotebookPath(relPath); + return { relPath, fullPath }; + } + async ensureTodoFileExists(fullPath) { + const dir = path8.dirname(fullPath); + await fs6.mkdir(dir, { recursive: true }); + try { + await fs6.access(fullPath); + } catch { + await fs6.writeFile(fullPath, "", "utf-8"); + } + } + async loadAndParseTodoFile(year, month) { + const { fullPath } = this.getTodoFilePath(year, month); + try { + await fs6.access(fullPath); + } catch { + throw new NotFoundError("TODO file not found"); + } + const content = await fs6.readFile(fullPath, "utf-8"); + return { fullPath, dayTodos: parseTodoContent(content) }; + } + async saveTodoFile(fullPath, dayTodos) { + const content = generateTodoContent(dayTodos); + await fs6.writeFile(fullPath, content, "utf-8"); + } + async getTodo(year, month) { + const { fullPath } = this.getTodoFilePath(year, month); + let dayTodos = []; + try { + await fs6.access(fullPath); + const content = await fs6.readFile(fullPath, "utf-8"); + dayTodos = parseTodoContent(content); + } catch { + } + const now = /* @__PURE__ */ new Date(); + const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}`; + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, "0")}-${yesterday.getDate().toString().padStart(2, "0")}`; + if (year === now.getFullYear() && month === now.getMonth() + 1) { + const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr); + if (migrated) { + const newContent = generateTodoContent(dayTodos); + await this.ensureTodoFileExists(fullPath); + await fs6.writeFile(fullPath, newContent, "utf-8"); + } + } + return { dayTodos, year, month }; + } + migrateIncompleteItems(dayTodos, todayStr, yesterdayStr) { + let migrated = false; + const yesterdayTodo = dayTodos.find((d) => d.date === yesterdayStr); + if (yesterdayTodo) { + const incompleteItems = yesterdayTodo.items.filter((item) => !item.completed); + if (incompleteItems.length > 0) { + const todayTodo = dayTodos.find((d) => d.date === todayStr); + if (todayTodo) { + const existingIds = new Set(todayTodo.items.map((i) => i.id)); + const itemsToAdd = incompleteItems.map((item, idx) => ({ + ...item, + id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id + })); + todayTodo.items = [...itemsToAdd, ...todayTodo.items]; + } else { + dayTodos.push({ + date: todayStr, + items: incompleteItems.map((item, idx) => ({ + ...item, + id: `${todayStr}-migrated-${idx}` + })) + }); + } + yesterdayTodo.items = yesterdayTodo.items.filter((item) => item.completed); + if (yesterdayTodo.items.length === 0) { + const index = dayTodos.findIndex((d) => d.date === yesterdayStr); + if (index !== -1) { + dayTodos.splice(index, 1); + } + } + migrated = true; + } + } + return migrated; + } + async saveTodo(year, month, dayTodos) { + const { fullPath } = this.getTodoFilePath(year, month); + await this.ensureTodoFileExists(fullPath); + const content = generateTodoContent(dayTodos); + await fs6.writeFile(fullPath, content, "utf-8"); + } + async addTodo(year, month, date, todoContent) { + const { fullPath } = this.getTodoFilePath(year, month); + await this.ensureTodoFileExists(fullPath); + let fileContent = await fs6.readFile(fullPath, "utf-8"); + const dayTodos = parseTodoContent(fileContent); + const existingDay = dayTodos.find((d) => d.date === date); + if (existingDay) { + const newId = `${date}-${existingDay.items.length}`; + existingDay.items.push({ + id: newId, + content: todoContent, + completed: false + }); + } else { + dayTodos.push({ + date, + items: [{ + id: `${date}-0`, + content: todoContent, + completed: false + }] + }); + } + fileContent = generateTodoContent(dayTodos); + await fs6.writeFile(fullPath, fileContent, "utf-8"); + return dayTodos; + } + async toggleTodo(year, month, date, itemIndex, completed) { + const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month); + const day = dayTodos.find((d) => d.date === date); + if (!day || itemIndex >= day.items.length) { + throw new NotFoundError("TODO item not found"); + } + day.items[itemIndex].completed = completed; + await this.saveTodoFile(fullPath, dayTodos); + return dayTodos; + } + async updateTodo(year, month, date, itemIndex, newContent) { + const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month); + const day = dayTodos.find((d) => d.date === date); + if (!day || itemIndex >= day.items.length) { + throw new NotFoundError("TODO item not found"); + } + day.items[itemIndex].content = newContent; + await this.saveTodoFile(fullPath, dayTodos); + return dayTodos; + } + async deleteTodo(year, month, date, itemIndex) { + const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month); + const dayIndex = dayTodos.findIndex((d) => d.date === date); + if (dayIndex === -1) { + throw new NotFoundError("Day not found"); + } + const day = dayTodos[dayIndex]; + if (itemIndex >= day.items.length) { + throw new NotFoundError("TODO item not found"); + } + day.items.splice(itemIndex, 1); + if (day.items.length === 0) { + dayTodos.splice(dayIndex, 1); + } + await this.saveTodoFile(fullPath, dayTodos); + return dayTodos; + } +}; + +// api/modules/todo/routes.ts +import express6 from "express"; + +// api/modules/todo/schemas.ts +import { z as z3 } from "zod"; +var todoItemSchema = z3.object({ + id: z3.string(), + content: z3.string(), + completed: z3.boolean() +}); +var dayTodoSchema = z3.object({ + date: z3.string(), + items: z3.array(todoItemSchema) +}); +var getTodoQuerySchema = z3.object({ + year: z3.string().optional(), + month: z3.string().optional() +}); +var saveTodoSchema = z3.object({ + year: z3.number().int().positive(), + month: z3.number().int().min(1).max(12), + dayTodos: z3.array(dayTodoSchema) +}); +var addTodoSchema = z3.object({ + year: z3.number().int().positive(), + month: z3.number().int().min(1).max(12), + date: z3.string(), + content: z3.string() +}); +var toggleTodoSchema = z3.object({ + year: z3.number().int().positive(), + month: z3.number().int().min(1).max(12), + date: z3.string(), + itemIndex: z3.number().int().nonnegative(), + completed: z3.boolean() +}); +var updateTodoSchema = z3.object({ + year: z3.number().int().positive(), + month: z3.number().int().min(1).max(12), + date: z3.string(), + itemIndex: z3.number().int().nonnegative(), + content: z3.string() +}); +var deleteTodoSchema = z3.object({ + year: z3.number().int().positive(), + month: z3.number().int().min(1).max(12), + date: z3.string(), + itemIndex: z3.number().int().nonnegative() +}); + +// api/modules/todo/routes.ts +var createTodoRoutes = (deps) => { + const router10 = express6.Router(); + const { todoService: todoService2 } = deps; + router10.get( + "/", + validateQuery(getTodoQuerySchema), + asyncHandler(async (req, res) => { + const year = parseInt(req.query.year) || (/* @__PURE__ */ new Date()).getFullYear(); + const month = parseInt(req.query.month) || (/* @__PURE__ */ new Date()).getMonth() + 1; + const result = await todoService2.getTodo(year, month); + successResponse(res, result); + }) + ); + router10.post( + "/save", + validateBody(saveTodoSchema), + asyncHandler(async (req, res) => { + const { year, month, dayTodos } = req.body; + await todoService2.saveTodo(year, month, dayTodos); + successResponse(res, null); + }) + ); + router10.post( + "/add", + validateBody(addTodoSchema), + asyncHandler(async (req, res) => { + const { year, month, date, content: todoContent } = req.body; + const dayTodos = await todoService2.addTodo(year, month, date, todoContent); + successResponse(res, { dayTodos }); + }) + ); + router10.post( + "/toggle", + validateBody(toggleTodoSchema), + asyncHandler(async (req, res) => { + const { year, month, date, itemIndex, completed } = req.body; + const dayTodos = await todoService2.toggleTodo(year, month, date, itemIndex, completed); + successResponse(res, { dayTodos }); + }) + ); + router10.post( + "/update", + validateBody(updateTodoSchema), + asyncHandler(async (req, res) => { + const { year, month, date, itemIndex, content: newContent } = req.body; + const dayTodos = await todoService2.updateTodo(year, month, date, itemIndex, newContent); + successResponse(res, { dayTodos }); + }) + ); + router10.delete( + "/delete", + validateBody(deleteTodoSchema), + asyncHandler(async (req, res) => { + const { year, month, date, itemIndex } = req.body; + const dayTodos = await todoService2.deleteTodo(year, month, date, itemIndex); + successResponse(res, { dayTodos }); + }) + ); + return router10; +}; +var todoService = new TodoService(); +var routes_default6 = createTodoRoutes({ todoService }); + +// shared/modules/time-tracking/api.ts +var TIME_TRACKING_ENDPOINTS = defineEndpoints({ + current: { path: "/current", method: "GET" }, + event: { path: "/event", method: "POST" }, + day: { path: "/day/:date", method: "GET" }, + week: { path: "/week/:startDate", method: "GET" }, + month: { path: "/month/:yearMonth", method: "GET" }, + year: { path: "/year/:year", method: "GET" }, + stats: { path: "/stats", method: "GET" } +}); + +// shared/modules/time-tracking/index.ts +var TIME_TRACKING_MODULE = defineApiModule({ + id: "time-tracking", + name: "\u65F6\u95F4\u7EDF\u8BA1", + basePath: "/time", + order: 20, + version: "1.0.0", + endpoints: TIME_TRACKING_ENDPOINTS +}); + +// api/modules/time-tracking/sessionPersistence.ts +import path9 from "path"; +var TIME_ROOT = path9.join(NOTEBOOK_ROOT, "time"); + +// api/modules/time-tracking/routes.ts +import express7 from "express"; + +// shared/modules/recycle-bin/api.ts +var RECYCLE_BIN_ENDPOINTS = defineEndpoints({ + list: { path: "/", method: "GET" }, + restore: { path: "/restore", method: "POST" }, + permanent: { path: "/permanent", method: "DELETE" }, + empty: { path: "/empty", method: "DELETE" } +}); + +// shared/modules/recycle-bin/index.ts +var RECYCLE_BIN_MODULE = defineApiModule({ + id: "recycle-bin", + name: "\u56DE\u6536\u7AD9", + basePath: "/recycle-bin", + order: 40, + version: "1.0.0", + endpoints: RECYCLE_BIN_ENDPOINTS +}); + +// api/modules/recycle-bin/routes.ts +import express8 from "express"; +import fs8 from "fs/promises"; +import path11 from "path"; + +// api/modules/recycle-bin/recycleBinService.ts +import fs7 from "fs/promises"; +import path10 from "path"; +async function restoreFile(srcPath, destPath, deletedDate, year, month, day) { + const { fullPath: imagesDir } = resolveNotebookPath(`images/${year}/${month}/${day}`); + let content = await fs7.readFile(srcPath, "utf-8"); + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + let match; + const imageReplacements = []; + while ((match = imageRegex.exec(content)) !== null) { + const imagePath = match[2]; + const imageName = path10.basename(imagePath); + const rbImageName = `${deletedDate}_${imageName}`; + const { fullPath: srcImagePath } = resolveNotebookPath(`RB/${rbImageName}`); + try { + await fs7.access(srcImagePath); + await fs7.mkdir(imagesDir, { recursive: true }); + const destImagePath = path10.join(imagesDir, imageName); + await fs7.rename(srcImagePath, destImagePath); + const newImagePath = `images/${year}/${month}/${day}/${imageName}`; + imageReplacements.push({ oldPath: imagePath, newPath: newImagePath }); + } catch { + } + } + for (const { oldPath, newPath } of imageReplacements) { + content = content.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), newPath); + } + await fs7.writeFile(destPath, content, "utf-8"); + await fs7.unlink(srcPath); +} +async function restoreFolder(srcPath, destPath, deletedDate, year, month, day) { + await fs7.mkdir(destPath, { recursive: true }); + const entries = await fs7.readdir(srcPath, { withFileTypes: true }); + for (const entry of entries) { + const srcEntryPath = path10.join(srcPath, entry.name); + const destEntryPath = path10.join(destPath, entry.name); + if (entry.isDirectory()) { + await restoreFolder(srcEntryPath, destEntryPath, deletedDate, year, month, day); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) { + await restoreFile(srcEntryPath, destEntryPath, deletedDate, year, month, day); + } else { + await fs7.rename(srcEntryPath, destEntryPath); + } + } + const remaining = await fs7.readdir(srcPath); + if (remaining.length === 0) { + await fs7.rmdir(srcPath); + } +} + +// api/modules/recycle-bin/routes.ts +var router6 = express8.Router(); +router6.get( + "/", + asyncHandler(async (req, res) => { + const { fullPath: rbDir } = resolveNotebookPath("RB"); + try { + await fs8.access(rbDir); + } catch { + successResponse(res, { groups: [] }); + return; + } + const entries = await fs8.readdir(rbDir, { withFileTypes: true }); + const items = []; + for (const entry of entries) { + const match = entry.name.match(/^(\d{8})_(.+)$/); + if (!match) continue; + const [, dateStr, originalName] = match; + if (entry.isDirectory()) { + items.push({ + name: entry.name, + originalName, + type: "dir", + deletedDate: dateStr, + path: `RB/${entry.name}` + }); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) { + items.push({ + name: entry.name, + originalName, + type: "file", + deletedDate: dateStr, + path: `RB/${entry.name}` + }); + } + } + const groupedMap = /* @__PURE__ */ new Map(); + for (const item of items) { + const existing = groupedMap.get(item.deletedDate) || []; + existing.push(item); + groupedMap.set(item.deletedDate, existing); + } + const groups = Array.from(groupedMap.entries()).map(([date, items2]) => ({ + date, + items: items2.sort((a, b) => a.originalName.localeCompare(b.originalName)) + })).sort((a, b) => b.date.localeCompare(a.date)); + successResponse(res, { groups }); + }) +); +router6.post( + "/restore", + asyncHandler(async (req, res) => { + const { path: relPath, type } = req.body; + if (!relPath || !type) { + throw new ValidationError("Path and type are required"); + } + const { fullPath: itemPath } = resolveNotebookPath(relPath); + try { + await fs8.access(itemPath); + } catch { + throw new NotFoundError("Item not found in recycle bin"); + } + const match = path11.basename(itemPath).match(/^(\d{8})_(.+)$/); + if (!match) { + throw new BadRequestError("Invalid recycle bin item name"); + } + const [, dateStr, originalName] = match; + const year = dateStr.substring(0, 4); + const month = dateStr.substring(4, 6); + const day = dateStr.substring(6, 8); + const { fullPath: markdownsDir } = resolveNotebookPath("markdowns"); + await fs8.mkdir(markdownsDir, { recursive: true }); + const destPath = path11.join(markdownsDir, originalName); + const existing = await fs8.stat(destPath).catch(() => null); + if (existing) { + throw new AlreadyExistsError("A file or folder with this name already exists"); + } + if (type === "dir") { + await restoreFolder(itemPath, destPath, dateStr, year, month, day); + } else { + await restoreFile(itemPath, destPath, dateStr, year, month, day); + } + successResponse(res, null); + }) +); +router6.delete( + "/permanent", + asyncHandler(async (req, res) => { + const { path: relPath, type } = req.body; + if (!relPath || !type) { + throw new ValidationError("Path and type are required"); + } + const { fullPath: itemPath } = resolveNotebookPath(relPath); + try { + await fs8.access(itemPath); + } catch { + throw new NotFoundError("Item not found in recycle bin"); + } + if (type === "dir") { + await fs8.rm(itemPath, { recursive: true, force: true }); + } else { + await fs8.unlink(itemPath); + } + successResponse(res, null); + }) +); +router6.delete( + "/empty", + asyncHandler(async (req, res) => { + const { fullPath: rbDir } = resolveNotebookPath("RB"); + try { + await fs8.access(rbDir); + } catch { + successResponse(res, null); + return; + } + const entries = await fs8.readdir(rbDir, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path11.join(rbDir, entry.name); + if (entry.isDirectory()) { + await fs8.rm(entryPath, { recursive: true, force: true }); + } else { + await fs8.unlink(entryPath); + } + } + successResponse(res, null); + }) +); + +// shared/modules/pydemos/api.ts +var PYDEMOS_ENDPOINTS = defineEndpoints({ + list: { path: "/", method: "GET" }, + create: { path: "/create", method: "POST" }, + delete: { path: "/delete", method: "DELETE" }, + rename: { path: "/rename", method: "POST" } +}); + +// shared/modules/pydemos/index.ts +var PYDEMOS_MODULE = defineApiModule({ + id: "pydemos", + name: "Python Demos", + basePath: "/pydemos", + order: 50, + version: "1.0.0", + endpoints: PYDEMOS_ENDPOINTS +}); + +// api/modules/pydemos/routes.ts +import express9 from "express"; +import fs9 from "fs/promises"; +import path12 from "path"; +import multer from "multer"; + +// api/utils/tempDir.ts +import { existsSync, mkdirSync } from "fs"; +var tempDir = null; +var getTempDir = () => { + if (!tempDir) { + tempDir = PATHS.TEMP_ROOT; + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }); + } + } + return tempDir; +}; + +// api/modules/pydemos/routes.ts +var tempDir2 = getTempDir(); +var upload = multer({ + dest: tempDir2, + limits: { + fileSize: 50 * 1024 * 1024 + } +}); +var toPosixPath2 = (p) => p.replace(/\\/g, "/"); +var getYearPath = (year) => { + const relPath = `pydemos/${year}`; + const { fullPath } = resolveNotebookPath(relPath); + return { relPath, fullPath }; +}; +var getMonthPath = (year, month) => { + const monthStr = month.toString().padStart(2, "0"); + const relPath = `pydemos/${year}/${monthStr}`; + const { fullPath } = resolveNotebookPath(relPath); + return { relPath, fullPath }; +}; +var countFilesInDir = async (dirPath) => { + try { + const entries = await fs9.readdir(dirPath, { withFileTypes: true }); + return entries.filter((e) => e.isFile()).length; + } catch { + return 0; + } +}; +var createPyDemosRoutes = () => { + const router10 = express9.Router(); + router10.get( + "/", + validateQuery(listPyDemosQuerySchema), + asyncHandler(async (req, res) => { + const year = parseInt(req.query.year) || (/* @__PURE__ */ new Date()).getFullYear(); + const { fullPath: yearPath } = getYearPath(year); + const months = []; + try { + await fs9.access(yearPath); + } catch { + successResponse(res, { months }); + return; + } + const monthEntries = await fs9.readdir(yearPath, { withFileTypes: true }); + for (const monthEntry of monthEntries) { + if (!monthEntry.isDirectory()) continue; + const monthNum = parseInt(monthEntry.name); + if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) continue; + const monthPath = path12.join(yearPath, monthEntry.name); + const demoEntries = await fs9.readdir(monthPath, { withFileTypes: true }); + const demos = []; + for (const demoEntry of demoEntries) { + if (!demoEntry.isDirectory()) continue; + const demoPath = path12.join(monthPath, demoEntry.name); + const relDemoPath = `pydemos/${year}/${monthEntry.name}/${demoEntry.name}`; + let created; + try { + const stats = await fs9.stat(demoPath); + created = stats.birthtime.toISOString(); + } catch { + created = (/* @__PURE__ */ new Date()).toISOString(); + } + const fileCount = await countFilesInDir(demoPath); + demos.push({ + name: demoEntry.name, + path: toPosixPath2(relDemoPath), + created, + fileCount + }); + } + demos.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()); + if (demos.length > 0) { + months.push({ + month: monthNum, + demos + }); + } + } + months.sort((a, b) => a.month - b.month); + successResponse(res, { months }); + }) + ); + router10.post( + "/create", + upload.array("files"), + validateBody(createPyDemoSchema), + asyncHandler(async (req, res) => { + const { name, year, month, folderStructure } = req.body; + const yearNum = parseInt(year); + const monthNum = parseInt(month); + if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(name)) { + throw new ValidationError("Invalid name format"); + } + const { fullPath: monthPath, relPath: monthRelPath } = getMonthPath(yearNum, monthNum); + const demoPath = path12.join(monthPath, name); + const relDemoPath = `${monthRelPath}/${name}`; + try { + await fs9.access(demoPath); + throw new AlreadyExistsError("Demo already exists"); + } catch (err) { + if (isNodeError(err) && err.code === "ENOENT") { + } else if (err instanceof AlreadyExistsError) { + throw err; + } else { + throw err; + } + } + await fs9.mkdir(demoPath, { recursive: true }); + const files = req.files; + let fileCount = 0; + if (files && files.length > 0) { + let structure = {}; + if (folderStructure) { + try { + structure = JSON.parse(folderStructure); + } catch { + structure = {}; + } + } + for (const file of files) { + const relativePath = structure[file.originalname] || file.originalname; + const targetPath = path12.join(demoPath, relativePath); + const targetDir = path12.dirname(targetPath); + await fs9.mkdir(targetDir, { recursive: true }); + await fs9.copyFile(file.path, targetPath); + await fs9.unlink(file.path).catch(() => { + }); + fileCount++; + } + } + successResponse(res, { path: toPosixPath2(relDemoPath), fileCount }); + }) + ); + router10.delete( + "/delete", + validateBody(deletePyDemoSchema), + asyncHandler(async (req, res) => { + const { path: demoPath } = req.body; + if (!demoPath.startsWith("pydemos/")) { + throw new ValidationError("Invalid path"); + } + const { fullPath } = resolveNotebookPath(demoPath); + try { + await fs9.access(fullPath); + } catch { + throw new NotFoundError("Demo not found"); + } + await fs9.rm(fullPath, { recursive: true, force: true }); + successResponse(res, null); + }) + ); + router10.post( + "/rename", + validateBody(renamePyDemoSchema), + asyncHandler(async (req, res) => { + const { oldPath, newName } = req.body; + if (!oldPath.startsWith("pydemos/")) { + throw new ValidationError("Invalid path"); + } + if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(newName)) { + throw new ValidationError("Invalid name format"); + } + const { fullPath: oldFullPath } = resolveNotebookPath(oldPath); + try { + await fs9.access(oldFullPath); + } catch { + throw new NotFoundError("Demo not found"); + } + const parentDir = path12.dirname(oldFullPath); + const newFullPath = path12.join(parentDir, newName); + const newPath = toPosixPath2(path12.join(path12.dirname(oldPath), newName)); + try { + await fs9.access(newFullPath); + throw new AlreadyExistsError("Demo with this name already exists"); + } catch (err) { + if (isNodeError(err) && err.code === "ENOENT") { + } else if (err instanceof AlreadyExistsError) { + throw err; + } else { + throw err; + } + } + await fs9.rename(oldFullPath, newFullPath); + successResponse(res, { newPath }); + }) + ); + return router10; +}; +var routes_default8 = createPyDemosRoutes(); + +// api/modules/document-parser/index.ts +import express12 from "express"; + +// shared/modules/document-parser/index.ts +var DOCUMENT_PARSER_MODULE = defineApiModule({ + id: "document-parser", + name: "Document Parser", + basePath: "/document-parser", + order: 60, + version: "1.0.0", + frontend: { + enabled: false + }, + backend: { + enabled: true + } +}); + +// api/modules/document-parser/blogRoutes.ts +import express10 from "express"; +import path14 from "path"; +import fs11 from "fs/promises"; +import { existsSync as existsSync3 } from "fs"; +import axios from "axios"; + +// api/modules/document-parser/documentParser.ts +import path13 from "path"; +import { spawn } from "child_process"; +import fs10 from "fs/promises"; +import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs"; +if (!existsSync2(TEMP_ROOT)) { + mkdirSync2(TEMP_ROOT, { recursive: true }); +} +var createJobContext = async (prefix) => { + const now = /* @__PURE__ */ new Date(); + const jobDir = path13.join(TEMP_ROOT, `${prefix}_${formatTimestamp(now)}`); + await fs10.mkdir(jobDir, { recursive: true }); + const year = now.getFullYear(); + const month = pad2(now.getMonth() + 1); + const day = pad2(now.getDate()); + const imagesSubDir = `images/${year}/${month}/${day}`; + const destImagesDir = path13.join(NOTEBOOK_ROOT, imagesSubDir); + await fs10.mkdir(destImagesDir, { recursive: true }); + return { jobDir, now, imagesSubDir, destImagesDir }; +}; +var spawnPythonScript = async (options) => { + const { scriptPath, args, cwd, inputContent } = options; + return new Promise((resolve, reject) => { + const pythonProcess = spawn("python", ["-X", "utf8", scriptPath, ...args], { + cwd, + env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } + }); + let stdout = ""; + let stderr = ""; + pythonProcess.stdout.on("data", (data) => { + stdout += data.toString(); + }); + pythonProcess.stderr.on("data", (data) => { + stderr += data.toString(); + }); + pythonProcess.on("close", (code) => { + if (code !== 0) { + logger.error("Python script error:", stderr); + reject(new Error(`Process exited with code ${code}. Error: ${stderr}`)); + } else { + resolve(stdout); + } + }); + pythonProcess.on("error", (err) => { + reject(err); + }); + if (inputContent !== void 0) { + pythonProcess.stdin.write(inputContent); + pythonProcess.stdin.end(); + } + }); +}; +var findImageDestinations = (md) => { + const results = []; + let i = 0; + while (i < md.length) { + const bang = md.indexOf("![", i); + if (bang === -1) break; + const closeBracket = md.indexOf("]", bang + 2); + if (closeBracket === -1) break; + if (md[closeBracket + 1] !== "(") { + i = closeBracket + 1; + continue; + } + const urlStart = closeBracket + 2; + let depth = 1; + let j = urlStart; + for (; j < md.length; j++) { + const ch = md[j]; + if (ch === "(") depth++; + else if (ch === ")") { + depth--; + if (depth === 0) break; + } + } + if (depth !== 0) break; + results.push({ url: md.slice(urlStart, j), start: urlStart, end: j }); + i = j + 1; + } + return results; +}; +var applyReplacements = (md, replacements) => { + const sorted = [...replacements].sort((a, b) => b.start - a.start); + let result = md; + for (const r of sorted) { + result = `${result.slice(0, r.start)}${r.replacement}${result.slice(r.end)}`; + } + return result; +}; +var copyLocalImage = async (src, jobDir, htmlDir, destImagesDir, imagesSubDir, now) => { + const s0 = src.trim().replace(/^<|>$/g, ""); + if (!s0) return null; + let decoded = s0; + try { + decoded = decodeURI(s0); + } catch { + } + const s1 = decoded.replace(/\\/g, "/"); + const s2 = s1.startsWith("./") ? s1.slice(2) : s1; + const candidates = s2.startsWith("/") ? [path13.join(jobDir, s2.slice(1)), path13.join(htmlDir, s2.slice(1))] : [path13.resolve(htmlDir, s2), path13.resolve(jobDir, s2)]; + let foundFile = null; + for (const c of candidates) { + if (existsSync2(c)) { + foundFile = c; + break; + } + } + if (!foundFile) return null; + const ext = path13.extname(foundFile) || ".jpg"; + const baseName = formatTimestamp(now); + const newFilename = await getUniqueFilename(destImagesDir, baseName, ext); + const newPath = path13.join(destImagesDir, newFilename); + await fs10.copyFile(foundFile, newPath); + return { newLink: `/${imagesSubDir}/${newFilename}` }; +}; +var cleanupJob = async (jobDir, additionalPaths = []) => { + await fs10.rm(jobDir, { recursive: true, force: true }).catch(() => { + }); + for (const p of additionalPaths) { + await fs10.unlink(p).catch(() => { + }); + } +}; +var getScriptPath = (toolName, scriptName) => { + return path13.join(PROJECT_ROOT, "tools", toolName, scriptName); +}; +var ensureScriptExists = (scriptPath) => { + return existsSync2(scriptPath); +}; + +// api/modules/document-parser/blogRoutes.ts +var router7 = express10.Router(); +var tempDir3 = getTempDir(); +router7.post( + "/parse-local", + asyncHandler(async (req, res) => { + const { htmlPath, htmlDir, assetsDirName, assetsFiles, targetPath } = req.body; + if (!htmlPath || !htmlDir || !targetPath) { + throw new ValidationError("htmlPath, htmlDir and targetPath are required"); + } + let fullTargetPath; + try { + const resolved = resolveNotebookPath(targetPath); + fullTargetPath = resolved.fullPath; + } catch (error) { + throw error; + } + const scriptPath = getScriptPath("blog", "parse_blog.py"); + if (!ensureScriptExists(scriptPath)) { + throw new InternalError("Parser script not found"); + } + const jobContext = await createJobContext("blog"); + let htmlPathInJob = ""; + try { + htmlPathInJob = path14.join(jobContext.jobDir, "input.html"); + await fs11.copyFile(htmlPath, htmlPathInJob); + if (assetsDirName && assetsFiles && assetsFiles.length > 0) { + const assetsDirPath = path14.join(htmlDir, assetsDirName); + for (const relPath of assetsFiles) { + const srcPath = path14.join(assetsDirPath, relPath); + if (existsSync3(srcPath)) { + const destPath = path14.join(jobContext.jobDir, assetsDirName, relPath); + await fs11.mkdir(path14.dirname(destPath), { recursive: true }); + await fs11.copyFile(srcPath, destPath); + } + } + } + } catch (err) { + await cleanupJob(jobContext.jobDir); + throw err; + } + processHtmlInBackground({ + jobDir: jobContext.jobDir, + htmlPath: htmlPathInJob, + targetPath: fullTargetPath, + cwd: path14.dirname(scriptPath), + jobContext, + originalHtmlDir: htmlDir, + originalAssetsDirName: assetsDirName + }).catch((err) => { + logger.error("Background HTML processing failed:", err); + fs11.writeFile(fullTargetPath, `# \u89E3\u6790\u5931\u8D25 + +> \u9519\u8BEF\u4FE1\u606F: ${err.message}`, "utf-8").catch(() => { + }); + cleanupJob(jobContext.jobDir).catch(() => { + }); + }); + successResponse(res, { + message: "HTML parsing started in background.", + status: "processing" + }); + }) +); +async function processHtmlInBackground(args) { + const { jobDir, htmlPath, targetPath, cwd, jobContext, originalHtmlDir, originalAssetsDirName } = args; + try { + await spawnPythonScript({ + scriptPath: "parse_blog.py", + args: [htmlPath], + cwd + }); + const parsedPathObj = path14.parse(htmlPath); + const markdownPath = path14.join(parsedPathObj.dir, `${parsedPathObj.name}.md`); + if (!existsSync3(markdownPath)) { + throw new Error("Markdown result file not found"); + } + let mdContent = await fs11.readFile(markdownPath, "utf-8"); + const ctx = await jobContext; + const htmlDir = path14.dirname(htmlPath); + const replacements = []; + const destinations = findImageDestinations(mdContent); + for (const dest of destinations) { + const originalSrc = dest.url; + if (!originalSrc) continue; + if (originalSrc.startsWith("http://") || originalSrc.startsWith("https://")) { + try { + const response = await axios.get(originalSrc, { responseType: "arraybuffer", timeout: 1e4 }); + const contentType = response.headers["content-type"]; + let ext = ".jpg"; + if (contentType) { + if (contentType.includes("png")) ext = ".png"; + else if (contentType.includes("gif")) ext = ".gif"; + else if (contentType.includes("webp")) ext = ".webp"; + else if (contentType.includes("svg")) ext = ".svg"; + else if (contentType.includes("jpeg") || contentType.includes("jpg")) ext = ".jpg"; + } + const urlExt = path14.extname(originalSrc.split("?")[0]); + if (urlExt) ext = urlExt; + const baseName = formatTimestamp(ctx.now); + const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext); + const newPath = path14.join(ctx.destImagesDir, newFilename); + await fs11.writeFile(newPath, response.data); + replacements.push({ + start: dest.start, + end: dest.end, + original: originalSrc, + replacement: `/${ctx.imagesSubDir}/${newFilename}` + }); + } catch { + } + continue; + } + if (originalSrc.startsWith("data:")) continue; + let result = await copyLocalImage( + originalSrc, + jobDir, + htmlDir, + ctx.destImagesDir, + ctx.imagesSubDir, + ctx.now + ); + if (!result && originalHtmlDir && originalAssetsDirName) { + const srcWithFiles = originalSrc.replace(/^\.\//, "").replace(/^\//, ""); + const possiblePaths = [ + path14.join(originalHtmlDir, originalAssetsDirName, srcWithFiles), + path14.join(originalHtmlDir, originalAssetsDirName, path14.basename(srcWithFiles)) + ]; + for (const p of possiblePaths) { + if (existsSync3(p)) { + const ext = path14.extname(p) || ".jpg"; + const baseName = formatTimestamp(ctx.now); + const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext); + const newPath = path14.join(ctx.destImagesDir, newFilename); + await fs11.copyFile(p, newPath); + result = { newLink: `/${ctx.imagesSubDir}/${newFilename}` }; + break; + } + } + } + if (result) { + replacements.push({ + start: dest.start, + end: dest.end, + original: originalSrc, + replacement: result.newLink + }); + } + } + mdContent = applyReplacements(mdContent, replacements); + await fs11.writeFile(targetPath, mdContent, "utf-8"); + await fs11.unlink(markdownPath).catch(() => { + }); + } finally { + await cleanupJob(jobDir); + } +} + +// api/modules/document-parser/mineruRoutes.ts +import express11 from "express"; +import multer2 from "multer"; +import path15 from "path"; +import fs12 from "fs/promises"; +import { existsSync as existsSync4 } from "fs"; +var router8 = express11.Router(); +var tempDir4 = getTempDir(); +var upload2 = multer2({ + dest: tempDir4, + limits: { + fileSize: 50 * 1024 * 1024 + } +}); +router8.post( + "/parse", + upload2.single("file"), + asyncHandler(async (req, res) => { + if (!req.file) { + throw new ValidationError("File is required"); + } + const { targetPath } = req.body; + if (!targetPath) { + await fs12.unlink(req.file.path).catch(() => { + }); + throw new ValidationError("Target path is required"); + } + let fullTargetPath; + try { + const resolved = resolveNotebookPath(targetPath); + fullTargetPath = resolved.fullPath; + } catch (error) { + await fs12.unlink(req.file.path).catch(() => { + }); + throw error; + } + const scriptPath = getScriptPath("mineru", "mineru_parser.py"); + if (!ensureScriptExists(scriptPath)) { + await fs12.unlink(req.file.path).catch(() => { + }); + throw new InternalError("Parser script not found"); + } + processPdfInBackground(req.file.path, fullTargetPath, path15.dirname(scriptPath)).catch((err) => { + logger.error("Background PDF processing failed:", err); + fs12.writeFile(fullTargetPath, `# \u89E3\u6790\u5931\u8D25 + +> \u9519\u8BEF\u4FE1\u606F: ${err.message}`, "utf-8").catch(() => { + }); + }); + successResponse(res, { + message: "PDF upload successful. Parsing started in background.", + status: "processing" + }); + }) +); +async function processPdfInBackground(filePath, targetPath, cwd) { + try { + const output = await spawnPythonScript({ + scriptPath: "mineru_parser.py", + args: [filePath], + cwd + }); + const match = output.match(/JSON_RESULT:(.*)/); + if (!match) { + throw new Error("Failed to parse Python script output: JSON_RESULT not found"); + } + const result = JSON.parse(match[1]); + const markdownPath = result.markdown_file; + const outputDir = result.output_dir; + if (!existsSync4(markdownPath)) { + throw new Error("Markdown result file not found"); + } + let mdContent = await fs12.readFile(markdownPath, "utf-8"); + const imagesDir = path15.join(outputDir, "images"); + if (existsSync4(imagesDir)) { + const jobContext = await createJobContext("pdf_images"); + const destinations = findImageDestinations(mdContent); + const replacements = []; + for (const dest of destinations) { + const originalSrc = dest.url; + if (!originalSrc) continue; + const possibleFilenames = [originalSrc, path15.basename(originalSrc)]; + let foundFile = null; + for (const fname of possibleFilenames) { + const localPath = path15.join(imagesDir, fname); + if (existsSync4(localPath)) { + foundFile = localPath; + break; + } + const directPath = path15.join(outputDir, originalSrc); + if (existsSync4(directPath)) { + foundFile = directPath; + break; + } + } + if (foundFile) { + const ext = path15.extname(foundFile); + const baseName = formatTimestamp(jobContext.now); + const newFilename = await getUniqueFilename(jobContext.destImagesDir, baseName, ext); + const newPath = path15.join(jobContext.destImagesDir, newFilename); + await fs12.copyFile(foundFile, newPath); + replacements.push({ + start: dest.start, + end: dest.end, + original: originalSrc, + replacement: `${jobContext.imagesSubDir}/${newFilename}` + }); + } + } + mdContent = applyReplacements(mdContent, replacements); + } + await fs12.writeFile(targetPath, mdContent, "utf-8"); + await fs12.unlink(markdownPath).catch(() => { + }); + if (outputDir && outputDir.includes("temp")) { + await fs12.rm(outputDir, { recursive: true, force: true }).catch(() => { + }); + } + } finally { + await fs12.unlink(filePath).catch(() => { + }); + } +} + +// shared/modules/ai/index.ts +var AI_MODULE = defineApiModule({ + id: "ai", + name: "AI", + basePath: "/ai", + order: 70, + version: "1.0.0", + frontend: { + enabled: false + }, + backend: { + enabled: true + } +}); + +// api/modules/ai/routes.ts +import express13 from "express"; +import { spawn as spawn2 } from "child_process"; +import path16 from "path"; +import { fileURLToPath as fileURLToPath2 } from "url"; +import fs13 from "fs/promises"; +import fsSync from "fs"; +var __filename2 = fileURLToPath2(import.meta.url); +var __dirname2 = path16.dirname(__filename2); +var router9 = express13.Router(); +var PYTHON_TIMEOUT_MS = 3e4; +var spawnPythonWithTimeout = (scriptPath, args, stdinContent, timeoutMs = PYTHON_TIMEOUT_MS) => { + return new Promise((resolve, reject) => { + const pythonProcess = spawn2("python", args, { + env: { ...process.env } + }); + let stdout = ""; + let stderr = ""; + let timeoutId = null; + const cleanup = () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + timeoutId = setTimeout(() => { + cleanup(); + pythonProcess.kill(); + reject(new Error(`Python script timed out after ${timeoutMs}ms`)); + }, timeoutMs); + pythonProcess.stdout.on("data", (data) => { + stdout += data.toString(); + }); + pythonProcess.stderr.on("data", (data) => { + stderr += data.toString(); + }); + pythonProcess.on("close", (code) => { + cleanup(); + if (code !== 0) { + reject(new Error(`Python script exited with code ${code}. Stderr: ${stderr}`)); + } else { + resolve(stdout); + } + }); + pythonProcess.on("error", (err) => { + cleanup(); + reject(new Error(`Failed to start python process: ${err.message}`)); + }); + pythonProcess.stdin.write(stdinContent); + pythonProcess.stdin.end(); + }); +}; +router9.post( + "/doubao", + asyncHandler(async (req, res) => { + const { task, path: relPath } = req.body; + if (!task) throw new ValidationError("Task is required"); + if (!relPath) throw new ValidationError("Path is required"); + const { fullPath } = resolveNotebookPath(relPath); + try { + await fs13.access(fullPath); + } catch { + throw new NotFoundError("File not found"); + } + const content = await fs13.readFile(fullPath, "utf-8"); + const projectRoot = path16.resolve(__dirname2, "..", "..", ".."); + const scriptPath = path16.join(projectRoot, "tools", "doubao", "main.py"); + if (!fsSync.existsSync(scriptPath)) { + throw new InternalError(`Python script not found: ${scriptPath}`); + } + try { + const result = await spawnPythonWithTimeout(scriptPath, ["--task", task], content); + await fs13.writeFile(fullPath, result, "utf-8"); + successResponse(res, { message: "Task completed successfully" }); + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + throw new InternalError(`AI task failed: ${message}`); + } + }) +); + +// shared/modules/remote/api.ts +var REMOTE_ENDPOINTS = defineEndpoints({ + getConfig: { path: "/config", method: "GET" }, + saveConfig: { path: "/config", method: "POST" }, + getScreenshot: { path: "/screenshot", method: "GET" }, + saveScreenshot: { path: "/screenshot", method: "POST" }, + getData: { path: "/data", method: "GET" }, + saveData: { path: "/data", method: "POST" } +}); + +// shared/modules/remote/index.ts +var REMOTE_MODULE = defineApiModule({ + id: "remote", + name: "\u8FDC\u7A0B", + basePath: "/remote", + order: 25, + version: "1.0.0", + endpoints: REMOTE_ENDPOINTS +}); + +// api/modules/remote/service.ts +import fs14 from "fs/promises"; +import path17 from "path"; +var REMOTE_DIR = "remote"; +var RemoteService = class { + constructor(deps = {}) { + this.deps = deps; + } + getRemoteDir() { + const { fullPath } = resolveNotebookPath(REMOTE_DIR); + return { relPath: REMOTE_DIR, fullPath }; + } + getDeviceDir(deviceName) { + const safeName = this.sanitizeFileName(deviceName); + const { fullPath } = resolveNotebookPath(path17.join(REMOTE_DIR, safeName)); + return { relPath: path17.join(REMOTE_DIR, safeName), fullPath }; + } + sanitizeFileName(name) { + return name.replace(/[<>:"/\\|?*]/g, "_").trim() || "unnamed"; + } + getDeviceConfigPath(deviceName) { + const { fullPath } = this.getDeviceDir(deviceName); + return path17.join(fullPath, "config.json"); + } + getDeviceScreenshotPath(deviceName) { + const { fullPath } = this.getDeviceDir(deviceName); + return path17.join(fullPath, "screenshot.png"); + } + getDeviceDataPath(deviceName) { + const { fullPath } = this.getDeviceDir(deviceName); + return path17.join(fullPath, "data.json"); + } + async ensureDir(dirPath) { + await fs14.mkdir(dirPath, { recursive: true }); + } + async getDeviceNames() { + const { fullPath } = this.getRemoteDir(); + try { + const entries = await fs14.readdir(fullPath, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); + return dirs; + } catch { + return []; + } + } + async getConfig() { + const deviceNames = await this.getDeviceNames(); + const devices = await Promise.all( + deviceNames.map(async (name) => { + try { + const configPath = this.getDeviceConfigPath(name); + const content = await fs14.readFile(configPath, "utf-8"); + const deviceConfig = JSON.parse(content); + return { + id: deviceConfig.id || name, + deviceName: name, + serverHost: deviceConfig.serverHost || "", + desktopPort: deviceConfig.desktopPort || 3e3, + gitPort: deviceConfig.gitPort || 3001 + }; + } catch { + return { + id: name, + deviceName: name, + serverHost: "", + desktopPort: 3e3, + gitPort: 3001 + }; + } + }) + ); + return { devices }; + } + async saveConfig(config2) { + const { fullPath: remoteDirFullPath } = this.getRemoteDir(); + await this.ensureDir(remoteDirFullPath); + const existingDevices = await this.getDeviceNames(); + const newDeviceNames = config2.devices.map((d) => this.sanitizeFileName(d.deviceName)); + for (const oldDevice of existingDevices) { + if (!newDeviceNames.includes(oldDevice)) { + try { + const oldDir = path17.join(remoteDirFullPath, oldDevice); + await fs14.rm(oldDir, { recursive: true, force: true }); + } catch { + } + } + } + for (const device of config2.devices) { + const deviceDir = this.getDeviceDir(device.deviceName); + await this.ensureDir(deviceDir.fullPath); + const deviceConfigPath = this.getDeviceConfigPath(device.deviceName); + const deviceConfig = { + id: device.id, + serverHost: device.serverHost, + desktopPort: device.desktopPort, + gitPort: device.gitPort + }; + await fs14.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), "utf-8"); + } + } + async getScreenshot(deviceName) { + if (!deviceName) { + return null; + } + const screenshotPath = this.getDeviceScreenshotPath(deviceName); + try { + return await fs14.readFile(screenshotPath); + } catch { + return null; + } + } + async saveScreenshot(dataUrl, deviceName) { + console.log("[RemoteService] saveScreenshot:", { deviceName, dataUrlLength: dataUrl?.length }); + if (!deviceName || deviceName.trim() === "") { + console.warn("[RemoteService] saveScreenshot skipped: no deviceName"); + return; + } + const deviceDir = this.getDeviceDir(deviceName); + await this.ensureDir(deviceDir.fullPath); + const base64Data = dataUrl.replace(/^data:image\/png;base64,/, ""); + const buffer = Buffer.from(base64Data, "base64"); + const screenshotPath = this.getDeviceScreenshotPath(deviceName); + await fs14.writeFile(screenshotPath, buffer); + } + async getData(deviceName) { + if (!deviceName || deviceName.trim() === "") { + return null; + } + const dataPath = this.getDeviceDataPath(deviceName); + try { + const content = await fs14.readFile(dataPath, "utf-8"); + return JSON.parse(content); + } catch { + return null; + } + } + async saveData(data, deviceName) { + if (!deviceName || deviceName.trim() === "") { + console.warn("[RemoteService] saveData skipped: no deviceName"); + return; + } + const deviceDir = this.getDeviceDir(deviceName); + await this.ensureDir(deviceDir.fullPath); + const dataPath = this.getDeviceDataPath(deviceName); + await fs14.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8"); + } +}; + +// api/modules/remote/routes.ts +import express14 from "express"; +var createRemoteRoutes = (deps) => { + const router10 = express14.Router(); + const { remoteService: remoteService2 } = deps; + router10.get( + "/config", + asyncHandler(async (req, res) => { + const config2 = await remoteService2.getConfig(); + successResponse(res, config2); + }) + ); + router10.post( + "/config", + asyncHandler(async (req, res) => { + const config2 = req.body; + await remoteService2.saveConfig(config2); + successResponse(res, null); + }) + ); + router10.get( + "/screenshot", + asyncHandler(async (req, res) => { + const deviceName = req.query.device; + const buffer = await remoteService2.getScreenshot(deviceName); + if (!buffer) { + return successResponse(res, ""); + } + const base64 = `data:image/png;base64,${buffer.toString("base64")}`; + successResponse(res, base64); + }) + ); + router10.post( + "/screenshot", + asyncHandler(async (req, res) => { + const { dataUrl, deviceName } = req.body; + console.log("[Remote] saveScreenshot called:", { deviceName, hasDataUrl: !!dataUrl }); + await remoteService2.saveScreenshot(dataUrl, deviceName); + successResponse(res, null); + }) + ); + router10.get( + "/data", + asyncHandler(async (req, res) => { + const deviceName = req.query.device; + const data = await remoteService2.getData(deviceName); + successResponse(res, data); + }) + ); + router10.post( + "/data", + asyncHandler(async (req, res) => { + const { deviceName, lastConnected } = req.body; + const data = {}; + if (lastConnected !== void 0) { + data.lastConnected = lastConnected; + } + await remoteService2.saveData(data, deviceName); + successResponse(res, null); + }) + ); + return router10; +}; +var remoteService = new RemoteService(); +var routes_default9 = createRemoteRoutes({ remoteService }); + +// import("./**/*/index.js") in api/modules/index.ts +var globImport_index_js = __glob({}); + +// api/modules/index.ts +var __filename3 = fileURLToPath3(import.meta.url); +var __dirname3 = dirname(__filename3); +var moduleFactoryPattern = /^create\w+Module$/; +async function discoverModules() { + const modules = []; + const entries = readdirSync(__dirname3); + for (const entry of entries) { + const entryPath = join(__dirname3, entry); + try { + const stats = statSync(entryPath); + if (!stats.isDirectory()) { + continue; + } + const moduleIndexPath = join(entryPath, "index.ts"); + let moduleIndexStats; + try { + moduleIndexStats = statSync(moduleIndexPath); + } catch { + continue; + } + if (!moduleIndexStats.isFile()) { + continue; + } + const moduleExports = await globImport_index_js(`./${entry}/index.js`); + for (const exportName of Object.keys(moduleExports)) { + if (moduleFactoryPattern.test(exportName)) { + const factory = moduleExports[exportName]; + if (typeof factory === "function") { + const module = factory(); + modules.push(module); + } + } + } + } catch (error) { + console.warn(`[ModuleLoader] Failed to load module '${entry}':`, error); + } + } + modules.sort((a, b) => { + const orderA = a.metadata.order ?? 0; + const orderB = b.metadata.order ?? 0; + return orderA - orderB; + }); + return modules; +} +var apiModules = await discoverModules(); + +// api/infra/moduleValidator.ts +import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync5 } from "fs"; +import { join as join2, dirname as dirname2 } from "path"; +import { fileURLToPath as fileURLToPath4 } from "url"; + +// import("../../shared/modules/**/*/index.js") in api/infra/moduleValidator.ts +var globImport_shared_modules_index_js = __glob({}); + +// api/infra/moduleValidator.ts +var __filename4 = fileURLToPath4(import.meta.url); +var __dirname4 = dirname2(__filename4); +function getSharedModulesPath() { + const possiblePaths = [ + join2(__dirname4, "../../shared/modules"), + join2(__dirname4, "../../../shared/modules"), + join2(process?.resourcesPath || "", "shared/modules") + ]; + for (const p of possiblePaths) { + if (existsSync5(p)) { + return p; + } + } + return null; +} +function needsBackendImplementation(moduleDef) { + return moduleDef.backend?.enabled !== false; +} +async function loadModuleDefinitions() { + const modules = []; + const sharedModulesPath = getSharedModulesPath(); + if (!sharedModulesPath) { + return modules; + } + const entries = readdirSync2(sharedModulesPath); + for (const entry of entries) { + const entryPath = join2(sharedModulesPath, entry); + const stat = statSync2(entryPath); + if (!stat.isDirectory()) { + continue; + } + try { + const moduleExports = await globImport_shared_modules_index_js(`../../shared/modules/${entry}/index.js`); + for (const key of Object.keys(moduleExports)) { + if (key.endsWith("_MODULE")) { + const moduleDef = moduleExports[key]; + if (moduleDef && moduleDef.id) { + modules.push({ + id: moduleDef.id, + name: moduleDef.name, + backend: moduleDef.backend + }); + } + } + } + } catch { + } + } + return modules; +} +async function validateModuleConsistency(apiModules2) { + const sharedModules = await loadModuleDefinitions(); + if (sharedModules.length === 0) { + console.log("[ModuleValidator] Skipping validation (shared modules not found, likely packaged mode)"); + return; + } + const apiModuleIds = new Set(apiModules2.map((m) => m.metadata.id)); + const errors = []; + for (const sharedModule of sharedModules) { + const needsBackend = needsBackendImplementation(sharedModule); + const hasApiModule = apiModuleIds.has(sharedModule.id); + if (needsBackend && !hasApiModule) { + errors.push( + `Module '${sharedModule.id}' is defined in shared but not registered in API modules` + ); + } + if (!needsBackend && hasApiModule) { + errors.push( + `Module '${sharedModule.id}' has backend disabled but is registered in API modules` + ); + } + } + if (errors.length > 0) { + throw new Error(`Module consistency validation failed: + - ${errors.join("\n - ")}`); + } + console.log( + `[ModuleValidator] \u2713 Module consistency validated: ${sharedModules.length} shared, ${apiModules2.length} API` + ); +} + +// api/app.ts +import path18 from "path"; +import fs15 from "fs"; +dotenv.config(); +var app = express15(); +var container = new ServiceContainer(); +var moduleManager = new ModuleManager(container); +app.use(cors()); +app.use(express15.json({ limit: "200mb" })); +app.use(express15.urlencoded({ extended: true, limit: "200mb" })); +app.use("/api/files", routes_default); +app.use("/api/events", routes_default2); +app.use("/api/settings", routes_default3); +app.use("/api/upload", routes_default4); +app.use("/api/search", routes_default5); +for (const module of apiModules) { + await moduleManager.register(module); +} +await validateModuleConsistency(apiModules); +for (const module of moduleManager.getAllModules()) { + await moduleManager.activate(module.metadata.id); + const router10 = await module.createRouter(container); + app.use("/api" + module.metadata.basePath, router10); +} +app.get("/background.png", (req, res, next) => { + const customBgPath = path18.join(NOTEBOOK_ROOT, ".config", "background.png"); + if (fs15.existsSync(customBgPath)) { + res.sendFile(customBgPath); + } else { + next(); + } +}); +app.use( + "/api/health", + (_req, res) => { + const response = { + success: true, + data: { message: "ok" } + }; + res.status(200).json(response); + } +); +app.use((req, res, next) => { + if (req.path.startsWith("/api")) { + const response = { + success: false, + error: { code: "NOT_FOUND", message: "API\u4E0D\u5B58\u5728" } + }; + res.status(404).json(response); + } else { + next(); + } +}); +app.use(errorHandler); +var app_default = app; + +// electron/server.ts +import path20 from "path"; +import express16 from "express"; +import { fileURLToPath as fileURLToPath5 } from "url"; + +// api/watcher/watcher.ts +import chokidar from "chokidar"; +import path19 from "path"; +var watcher = null; +var startWatcher = () => { + if (watcher) return; + logger.info(`Starting file watcher for: ${NOTEBOOK_ROOT}`); + watcher = chokidar.watch(NOTEBOOK_ROOT, { + ignored: /(^|[\/\\])\../, + persistent: true, + ignoreInitial: true + }); + const broadcast = (event, changedPath) => { + const rel = path19.relative(NOTEBOOK_ROOT, changedPath); + if (!rel || rel.startsWith("..") || path19.isAbsolute(rel)) return; + logger.info(`File event: ${event} - ${rel}`); + eventBus.broadcast({ event, path: toPosixPath(rel) }); + }; + watcher.on("add", (p) => broadcast("add", p)).on("change", (p) => broadcast("change", p)).on("unlink", (p) => broadcast("unlink", p)).on("addDir", (p) => broadcast("addDir", p)).on("unlinkDir", (p) => broadcast("unlinkDir", p)).on("ready", () => logger.info("File watcher ready")).on("error", (err) => logger.error("File watcher error:", err)); +}; + +// electron/server.ts +var __filename5 = fileURLToPath5(import.meta.url); +var __dirname5 = path20.dirname(__filename5); +startWatcher(); +var distPath = path20.join(__dirname5, "../dist"); +app_default.use(express16.static(distPath)); +app_default.get("*", (req, res) => { + res.sendFile(path20.join(distPath, "index.html")); +}); +var startServer = () => { + return new Promise((resolve, reject) => { + const server = app_default.listen(0, () => { + const address = server.address(); + const port = address.port; + logger.info(`Electron internal server running on port ${port}`); + resolve(port); + }); + server.on("error", (err) => { + logger.error("Failed to start server:", err); + reject(err); + }); + }); +}; +export { + startServer +}; diff --git a/dist-electron/main.js b/dist-electron/main.js new file mode 100644 index 0000000..efd708d --- /dev/null +++ b/dist-electron/main.js @@ -0,0 +1,316 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + +// electron/main.ts +import { app, BrowserWindow as BrowserWindow3, shell, ipcMain, dialog as dialog2, nativeTheme, globalShortcut, clipboard } from "electron"; +import path2 from "path"; +import { fileURLToPath, pathToFileURL } from "url"; +import fs2 from "fs"; +import log2 from "electron-log"; + +// electron/services/pdfGenerator.ts +import { BrowserWindow } from "electron"; +async function generatePdf(htmlContent) { + const printWin = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: false + // 与 main.ts 保持一致,确保脚本执行权限 + } + }); + try { + await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`); + await printWin.webContents.executeJavaScript(` + new Promise(resolve => { + const check = () => { + if (window.__PRINT_READY__) { + resolve(); + } else { + setTimeout(check, 100); + } + } + check(); + }) + `); + const pdfData = await printWin.webContents.printToPDF({ + printBackground: true, + pageSize: "A4", + margins: { top: 0, bottom: 0, left: 0, right: 0 } + }); + return pdfData; + } finally { + printWin.close(); + } +} + +// electron/services/htmlImport.ts +import { dialog } from "electron"; +import path from "path"; +import fs from "fs"; +import log from "electron-log"; +var selectHtmlFile = async (win) => { + if (!win) return { success: false, error: "No window found" }; + try { + const { filePaths, canceled } = await dialog.showOpenDialog(win, { + title: "\u9009\u62E9 HTML \u6587\u4EF6", + filters: [ + { name: "HTML Files", extensions: ["html", "htm"] } + ], + properties: ["openFile"] + }); + if (canceled || filePaths.length === 0) { + return { success: false, canceled: true }; + } + const htmlPath = filePaths[0]; + const htmlDir = path.dirname(htmlPath); + const htmlFileName = path.basename(htmlPath, path.extname(htmlPath)); + const assetsDirName = `${htmlFileName}_files`; + const assetsDirPath = path.join(htmlDir, assetsDirName); + const assetsFiles = []; + if (fs.existsSync(assetsDirPath)) { + const collectFiles = (dir, baseDir) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectFiles(fullPath, baseDir); + } else { + const relPath = path.relative(baseDir, fullPath); + assetsFiles.push(relPath); + } + } + }; + collectFiles(assetsDirPath, assetsDirPath); + } + return { + success: true, + htmlPath, + htmlDir, + htmlFileName, + assetsDirName, + assetsFiles + }; + } catch (error) { + log.error("Select HTML file failed:", error); + return { success: false, error: error.message }; + } +}; + +// electron/state.ts +var ElectronState = class { + constructor() { + __publicField(this, "state", { + mainWindow: null, + serverPort: 3001, + isDev: false + }); + } + getMainWindow() { + return this.state.mainWindow; + } + setMainWindow(window) { + this.state.mainWindow = window; + } + getServerPort() { + return this.state.serverPort; + } + setServerPort(port) { + this.state.serverPort = port; + } + isDevelopment() { + return this.state.isDev; + } + setDevelopment(isDev) { + this.state.isDev = isDev; + } + reset() { + this.state = { + mainWindow: null, + serverPort: 3001, + isDev: false + }; + } +}; +var electronState = new ElectronState(); + +// electron/main.ts +log2.initialize(); +var __filename = fileURLToPath(import.meta.url); +var __dirname = path2.dirname(__filename); +process.env.NOTEBOOK_ROOT = path2.join(app.getPath("documents"), "XCNote"); +if (!fs2.existsSync(process.env.NOTEBOOK_ROOT)) { + try { + fs2.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true }); + } catch (err) { + log2.error("Failed to create notebook directory:", err); + } +} +electronState.setDevelopment(!app.isPackaged); +var lastClipboardText = ""; +function startClipboardWatcher() { + lastClipboardText = clipboard.readText(); + setInterval(() => { + try { + const currentText = clipboard.readText(); + if (currentText && currentText !== lastClipboardText) { + lastClipboardText = currentText; + log2.info("Clipboard changed, syncing to remote"); + const win = electronState.getMainWindow(); + if (win) { + win.webContents.send("remote-clipboard-auto-sync", currentText); + } + } + } catch (e) { + } + }, 1e3); +} +async function createWindow() { + const initialSymbolColor = nativeTheme.shouldUseDarkColors ? "#ffffff" : "#000000"; + const mainWindow = new BrowserWindow3({ + width: 1280, + height: 800, + minWidth: 1600, + minHeight: 900, + autoHideMenuBar: true, + titleBarStyle: "hidden", + titleBarOverlay: { + color: "#00000000", + symbolColor: initialSymbolColor, + height: 32 + }, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: false, + webviewTag: true, + preload: path2.join(__dirname, "preload.cjs") + } + }); + electronState.setMainWindow(mainWindow); + mainWindow.setMenu(null); + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http:") || url.startsWith("https:")) { + shell.openExternal(url); + return { action: "deny" }; + } + return { action: "allow" }; + }); + if (electronState.isDevelopment()) { + log2.info("Loading development URL..."); + try { + await mainWindow.loadURL("http://localhost:5173"); + } catch (e) { + log2.error("Failed to load dev URL. Make sure npm run electron:dev is used.", e); + } + mainWindow.webContents.openDevTools(); + } else { + log2.info(`Loading production URL with port ${electronState.getServerPort()}...`); + await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`); + } +} +ipcMain.handle("export-pdf", async (event, title, htmlContent) => { + const win = BrowserWindow3.fromWebContents(event.sender); + if (!win) return { success: false, error: "No window found" }; + try { + const { filePath } = await dialog2.showSaveDialog(win, { + title: "\u5BFC\u51FA PDF", + defaultPath: `${title}.pdf`, + filters: [{ name: "PDF Files", extensions: ["pdf"] }] + }); + if (!filePath) return { success: false, canceled: true }; + if (!htmlContent) { + throw new Error("No HTML content provided for PDF export"); + } + const pdfData = await generatePdf(htmlContent); + fs2.writeFileSync(filePath, pdfData); + return { success: true, filePath }; + } catch (error) { + log2.error("Export PDF failed:", error); + return { success: false, error: error.message }; + } +}); +ipcMain.handle("select-html-file", async (event) => { + const win = BrowserWindow3.fromWebContents(event.sender); + return selectHtmlFile(win); +}); +ipcMain.handle("update-titlebar-buttons", async (event, symbolColor) => { + const win = BrowserWindow3.fromWebContents(event.sender); + if (win) { + win.setTitleBarOverlay({ symbolColor }); + return { success: true }; + } + return { success: false }; +}); +ipcMain.handle("clipboard-read-text", async () => { + try { + const text = clipboard.readText(); + return { success: true, text }; + } catch (error) { + log2.error("Clipboard read failed:", error); + return { success: false, error: error.message }; + } +}); +ipcMain.handle("clipboard-write-text", async (event, text) => { + try { + clipboard.writeText(text); + return { success: true }; + } catch (error) { + log2.error("Clipboard write failed:", error); + return { success: false, error: error.message }; + } +}); +async function startServer() { + if (electronState.isDevelopment()) { + log2.info("In dev mode, assuming external servers are running."); + return; + } + const serverPath = path2.join(__dirname, "../dist-api/server.js"); + const serverUrl = pathToFileURL(serverPath).href; + log2.info(`Starting internal server from: ${serverPath}`); + try { + const serverModule = await import(serverUrl); + if (serverModule.startServer) { + const port = await serverModule.startServer(); + electronState.setServerPort(port); + log2.info(`Internal server started successfully on port ${port}`); + } else { + log2.warn("startServer function not found in server module, using default port 3001"); + } + } catch (e) { + log2.error("Failed to start internal server:", e); + } +} +app.whenReady().then(async () => { + await startServer(); + await createWindow(); + startClipboardWatcher(); + globalShortcut.register("CommandOrControl+Shift+C", () => { + log2.info("Global shortcut: sync clipboard to remote"); + const win = electronState.getMainWindow(); + if (win) { + win.webContents.send("remote-clipboard-sync-to-remote"); + } + }); + globalShortcut.register("CommandOrControl+Shift+V", () => { + log2.info("Global shortcut: sync clipboard from remote"); + const win = electronState.getMainWindow(); + if (win) { + win.webContents.send("remote-clipboard-sync-from-remote"); + } + }); + app.on("activate", () => { + if (BrowserWindow3.getAllWindows().length === 0) { + createWindow(); + } + }); +}); +app.on("window-all-closed", () => { + globalShortcut.unregisterAll(); + if (process.platform !== "darwin") { + app.quit(); + } +}); +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/dist-electron/main.js.map b/dist-electron/main.js.map new file mode 100644 index 0000000..a13385b --- /dev/null +++ b/dist-electron/main.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../electron/main.ts","../electron/services/pdfGenerator.ts","../electron/services/htmlImport.ts","../electron/state.ts"],"sourcesContent":["import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron';\r\nimport path from 'path';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport fs from 'fs';\r\nimport log from 'electron-log';\r\nimport { generatePdf } from './services/pdfGenerator';\r\nimport { selectHtmlFile } from './services/htmlImport';\r\nimport { electronState } from './state';\r\n\r\nlog.initialize();\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nprocess.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCNote');\r\n\r\nif (!fs.existsSync(process.env.NOTEBOOK_ROOT)) {\r\n try {\r\n fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true });\r\n } catch (err) {\r\n log.error('Failed to create notebook directory:', err);\r\n }\r\n}\r\n\r\nelectronState.setDevelopment(!app.isPackaged);\r\n\r\nlet lastClipboardText = '';\r\n\r\nfunction startClipboardWatcher() {\r\n lastClipboardText = clipboard.readText();\r\n \r\n setInterval(() => {\r\n try {\r\n const currentText = clipboard.readText();\r\n if (currentText && currentText !== lastClipboardText) {\r\n lastClipboardText = currentText;\r\n log.info('Clipboard changed, syncing to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-auto-sync', currentText);\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n }, 1000);\r\n}\r\n\r\nasync function createWindow() {\r\n const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000';\r\n \r\n const mainWindow = new BrowserWindow({\r\n width: 1280,\r\n height: 800,\r\n minWidth: 1600,\r\n minHeight: 900,\r\n autoHideMenuBar: true,\r\n titleBarStyle: 'hidden',\r\n titleBarOverlay: {\r\n color: '#00000000',\r\n symbolColor: initialSymbolColor,\r\n height: 32,\r\n },\r\n webPreferences: {\r\n nodeIntegration: false,\r\n contextIsolation: true,\r\n sandbox: false,\r\n webviewTag: true,\r\n preload: path.join(__dirname, 'preload.cjs'),\r\n },\r\n });\r\n\r\n electronState.setMainWindow(mainWindow);\r\n mainWindow.setMenu(null);\r\n\r\n mainWindow.webContents.setWindowOpenHandler(({ url }) => {\r\n if (url.startsWith('http:') || url.startsWith('https:')) {\r\n shell.openExternal(url);\r\n return { action: 'deny' };\r\n }\r\n return { action: 'allow' };\r\n });\r\n\r\n if (electronState.isDevelopment()) {\r\n log.info('Loading development URL...');\r\n try {\r\n await mainWindow.loadURL('http://localhost:5173');\r\n } catch (e) {\r\n log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e);\r\n }\r\n mainWindow.webContents.openDevTools();\r\n } else {\r\n log.info(`Loading production URL with port ${electronState.getServerPort()}...`);\r\n await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`);\r\n }\r\n}\r\n\r\nipcMain.handle('export-pdf', async (event, title, htmlContent) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (!win) return { success: false, error: 'No window found' };\r\n\r\n try {\r\n const { filePath } = await dialog.showSaveDialog(win, {\r\n title: '导出 PDF',\r\n defaultPath: `${title}.pdf`,\r\n filters: [{ name: 'PDF Files', extensions: ['pdf'] }]\r\n });\r\n\r\n if (!filePath) return { success: false, canceled: true };\r\n\r\n if (!htmlContent) {\r\n throw new Error('No HTML content provided for PDF export');\r\n }\r\n\r\n const pdfData = await generatePdf(htmlContent);\r\n fs.writeFileSync(filePath, pdfData);\r\n\r\n return { success: true, filePath };\r\n } catch (error: any) {\r\n log.error('Export PDF failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('select-html-file', async (event) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n return selectHtmlFile(win);\r\n});\r\n\r\nipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => {\r\n const win = BrowserWindow.fromWebContents(event.sender);\r\n if (win) {\r\n win.setTitleBarOverlay({ symbolColor });\r\n return { success: true };\r\n }\r\n return { success: false };\r\n});\r\n\r\nipcMain.handle('clipboard-read-text', async () => {\r\n try {\r\n const text = clipboard.readText();\r\n return { success: true, text };\r\n } catch (error: any) {\r\n log.error('Clipboard read failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nipcMain.handle('clipboard-write-text', async (event, text: string) => {\r\n try {\r\n clipboard.writeText(text);\r\n return { success: true };\r\n } catch (error: any) {\r\n log.error('Clipboard write failed:', error);\r\n return { success: false, error: error.message };\r\n }\r\n});\r\n\r\nasync function startServer() {\r\n if (electronState.isDevelopment()) {\r\n log.info('In dev mode, assuming external servers are running.');\r\n return;\r\n }\r\n\r\n const serverPath = path.join(__dirname, '../dist-api/server.js');\r\n const serverUrl = pathToFileURL(serverPath).href;\r\n\r\n log.info(`Starting internal server from: ${serverPath}`);\r\n try {\r\n const serverModule = await import(serverUrl);\r\n if (serverModule.startServer) {\r\n const port = await serverModule.startServer();\r\n electronState.setServerPort(port);\r\n log.info(`Internal server started successfully on port ${port}`);\r\n } else {\r\n log.warn('startServer function not found in server module, using default port 3001');\r\n }\r\n } catch (e) {\r\n log.error('Failed to start internal server:', e);\r\n }\r\n}\r\n\r\napp.whenReady().then(async () => {\r\n await startServer();\r\n await createWindow();\r\n\r\n startClipboardWatcher();\r\n\r\n globalShortcut.register('CommandOrControl+Shift+C', () => {\r\n log.info('Global shortcut: sync clipboard to remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-to-remote');\r\n }\r\n });\r\n\r\n globalShortcut.register('CommandOrControl+Shift+V', () => {\r\n log.info('Global shortcut: sync clipboard from remote');\r\n const win = electronState.getMainWindow();\r\n if (win) {\r\n win.webContents.send('remote-clipboard-sync-from-remote');\r\n }\r\n });\r\n\r\n app.on('activate', () => {\r\n if (BrowserWindow.getAllWindows().length === 0) {\r\n createWindow();\r\n }\r\n });\r\n});\r\n\r\napp.on('window-all-closed', () => {\r\n globalShortcut.unregisterAll();\r\n if (process.platform !== 'darwin') {\r\n app.quit();\r\n }\r\n});\r\n","import { BrowserWindow } from 'electron';\n\n/**\n * 生成 PDF 的服务\n * @param htmlContent 完整的 HTML 字符串\n * @returns PDF 文件的二进制数据\n */\nexport async function generatePdf(htmlContent: string): Promise {\n const printWin = new BrowserWindow({\n show: false,\n webPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限\n }\n });\n\n try {\n await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);\n \n // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制)\n await printWin.webContents.executeJavaScript(`\n new Promise(resolve => {\n const check = () => {\n if (window.__PRINT_READY__) {\n resolve();\n } else {\n setTimeout(check, 100);\n }\n }\n check();\n })\n `);\n\n const pdfData = await printWin.webContents.printToPDF({\n printBackground: true,\n pageSize: 'A4',\n margins: { top: 0, bottom: 0, left: 0, right: 0 }\n });\n\n return pdfData;\n } finally {\n // 确保窗口被关闭,防止内存泄漏\n printWin.close();\n }\n}\n","import { dialog, BrowserWindow } from 'electron'\nimport path from 'path'\nimport fs from 'fs'\nimport log from 'electron-log'\n\nexport interface HtmlImportResult {\n success: boolean\n canceled?: boolean\n error?: string\n htmlPath?: string\n htmlDir?: string\n htmlFileName?: string\n assetsDirName?: string\n assetsFiles?: string[]\n}\n\nexport const selectHtmlFile = async (win: BrowserWindow | null): Promise => {\n if (!win) return { success: false, error: 'No window found' }\n\n try {\n const { filePaths, canceled } = await dialog.showOpenDialog(win, {\n title: '选择 HTML 文件',\n filters: [\n { name: 'HTML Files', extensions: ['html', 'htm'] }\n ],\n properties: ['openFile']\n })\n\n if (canceled || filePaths.length === 0) {\n return { success: false, canceled: true }\n }\n\n const htmlPath = filePaths[0]\n const htmlDir = path.dirname(htmlPath)\n const htmlFileName = path.basename(htmlPath, path.extname(htmlPath))\n \n const assetsDirName = `${htmlFileName}_files`\n const assetsDirPath = path.join(htmlDir, assetsDirName)\n \n const assetsFiles: string[] = []\n if (fs.existsSync(assetsDirPath)) {\n const collectFiles = (dir: string, baseDir: string) => {\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n collectFiles(fullPath, baseDir)\n } else {\n const relPath = path.relative(baseDir, fullPath)\n assetsFiles.push(relPath)\n }\n }\n }\n collectFiles(assetsDirPath, assetsDirPath)\n }\n\n return { \n success: true, \n htmlPath,\n htmlDir,\n htmlFileName,\n assetsDirName,\n assetsFiles\n }\n } catch (error: any) {\n log.error('Select HTML file failed:', error)\n return { success: false, error: error.message }\n }\n}\n","import { BrowserWindow } from 'electron'\n\ninterface ElectronAppState {\n mainWindow: BrowserWindow | null\n serverPort: number\n isDev: boolean\n}\n\nclass ElectronState {\n private state: ElectronAppState = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n\n getMainWindow(): BrowserWindow | null {\n return this.state.mainWindow\n }\n\n setMainWindow(window: BrowserWindow | null): void {\n this.state.mainWindow = window\n }\n\n getServerPort(): number {\n return this.state.serverPort\n }\n\n setServerPort(port: number): void {\n this.state.serverPort = port\n }\n\n isDevelopment(): boolean {\n return this.state.isDev\n }\n\n setDevelopment(isDev: boolean): void {\n this.state.isDev = isDev\n }\n\n reset(): void {\n this.state = {\n mainWindow: null,\n serverPort: 3001,\n isDev: false,\n }\n }\n}\n\nexport const electronState = new ElectronState()\n"],"mappings":";;;;;AAAA,SAAS,KAAK,iBAAAA,gBAAe,OAAO,SAAS,UAAAC,SAAQ,aAAa,gBAAgB,iBAAiB;AACnG,OAAOC,WAAU;AACjB,SAAS,eAAe,qBAAqB;AAC7C,OAAOC,SAAQ;AACf,OAAOC,UAAS;;;ACJhB,SAAS,qBAAqB;AAO9B,eAAsB,YAAY,aAA0C;AAC1E,QAAM,WAAW,IAAI,cAAc;AAAA,IACjC,MAAM;AAAA,IACN,gBAAgB;AAAA,MACd,iBAAiB;AAAA,MACjB,kBAAkB;AAAA,MAClB,SAAS;AAAA;AAAA,IACX;AAAA,EACF,CAAC;AAED,MAAI;AACF,UAAM,SAAS,QAAQ,gCAAgC,mBAAmB,WAAW,CAAC,EAAE;AAGxF,UAAM,SAAS,YAAY,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAW5C;AAED,UAAM,UAAU,MAAM,SAAS,YAAY,WAAW;AAAA,MACpD,iBAAiB;AAAA,MACjB,UAAU;AAAA,MACV,SAAS,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE;AAAA,IAClD,CAAC;AAED,WAAO;AAAA,EACT,UAAE;AAEA,aAAS,MAAM;AAAA,EACjB;AACF;;;AC7CA,SAAS,cAA6B;AACtC,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,SAAS;AAaT,IAAM,iBAAiB,OAAO,QAAyD;AAC5F,MAAI,CAAC,IAAK,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AAE5D,MAAI;AACF,UAAM,EAAE,WAAW,SAAS,IAAI,MAAM,OAAO,eAAe,KAAK;AAAA,MAC/D,OAAO;AAAA,MACP,SAAS;AAAA,QACP,EAAE,MAAM,cAAc,YAAY,CAAC,QAAQ,KAAK,EAAE;AAAA,MACpD;AAAA,MACA,YAAY,CAAC,UAAU;AAAA,IACzB,CAAC;AAED,QAAI,YAAY,UAAU,WAAW,GAAG;AACtC,aAAO,EAAE,SAAS,OAAO,UAAU,KAAK;AAAA,IAC1C;AAEA,UAAM,WAAW,UAAU,CAAC;AAC5B,UAAM,UAAU,KAAK,QAAQ,QAAQ;AACrC,UAAM,eAAe,KAAK,SAAS,UAAU,KAAK,QAAQ,QAAQ,CAAC;AAEnE,UAAM,gBAAgB,GAAG,YAAY;AACrC,UAAM,gBAAgB,KAAK,KAAK,SAAS,aAAa;AAEtD,UAAM,cAAwB,CAAC;AAC/B,QAAI,GAAG,WAAW,aAAa,GAAG;AAChC,YAAM,eAAe,CAAC,KAAa,YAAoB;AACrD,cAAM,UAAU,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAC3D,mBAAW,SAAS,SAAS;AAC3B,gBAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,cAAI,MAAM,YAAY,GAAG;AACvB,yBAAa,UAAU,OAAO;AAAA,UAChC,OAAO;AACL,kBAAM,UAAU,KAAK,SAAS,SAAS,QAAQ;AAC/C,wBAAY,KAAK,OAAO;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AACA,mBAAa,eAAe,aAAa;AAAA,IAC3C;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,OAAY;AACnB,QAAI,MAAM,4BAA4B,KAAK;AAC3C,WAAO,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ;AAAA,EAChD;AACF;;;AC5DA,IAAM,gBAAN,MAAoB;AAAA,EAApB;AACE,wBAAQ,SAA0B;AAAA,MAChC,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,OAAO;AAAA,IACT;AAAA;AAAA,EAEA,gBAAsC;AACpC,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,cAAc,QAAoC;AAChD,SAAK,MAAM,aAAa;AAAA,EAC1B;AAAA,EAEA,gBAAwB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,cAAc,MAAoB;AAChC,SAAK,MAAM,aAAa;AAAA,EAC1B;AAAA,EAEA,gBAAyB;AACvB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,eAAe,OAAsB;AACnC,SAAK,MAAM,QAAQ;AAAA,EACrB;AAAA,EAEA,QAAc;AACZ,SAAK,QAAQ;AAAA,MACX,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,OAAO;AAAA,IACT;AAAA,EACF;AACF;AAEO,IAAM,gBAAgB,IAAI,cAAc;;;AHvC/CC,KAAI,WAAW;AAEf,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAYC,MAAK,QAAQ,UAAU;AAEzC,QAAQ,IAAI,gBAAgBA,MAAK,KAAK,IAAI,QAAQ,WAAW,GAAG,QAAQ;AAExE,IAAI,CAACC,IAAG,WAAW,QAAQ,IAAI,aAAa,GAAG;AAC7C,MAAI;AACF,IAAAA,IAAG,UAAU,QAAQ,IAAI,eAAe,EAAE,WAAW,KAAK,CAAC;AAAA,EAC7D,SAAS,KAAK;AACZ,IAAAF,KAAI,MAAM,wCAAwC,GAAG;AAAA,EACvD;AACF;AAEA,cAAc,eAAe,CAAC,IAAI,UAAU;AAE5C,IAAI,oBAAoB;AAExB,SAAS,wBAAwB;AAC/B,sBAAoB,UAAU,SAAS;AAEvC,cAAY,MAAM;AAChB,QAAI;AACF,YAAM,cAAc,UAAU,SAAS;AACvC,UAAI,eAAe,gBAAgB,mBAAmB;AACpD,4BAAoB;AACpB,QAAAA,KAAI,KAAK,sCAAsC;AAC/C,cAAM,MAAM,cAAc,cAAc;AACxC,YAAI,KAAK;AACP,cAAI,YAAY,KAAK,8BAA8B,WAAW;AAAA,QAChE;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AAAA,IAEZ;AAAA,EACF,GAAG,GAAI;AACT;AAEA,eAAe,eAAe;AAC5B,QAAM,qBAAqB,YAAY,sBAAsB,YAAY;AAEzE,QAAM,aAAa,IAAIG,eAAc;AAAA,IACnC,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,iBAAiB;AAAA,MACf,OAAO;AAAA,MACP,aAAa;AAAA,MACb,QAAQ;AAAA,IACV;AAAA,IACA,gBAAgB;AAAA,MACd,iBAAiB;AAAA,MACjB,kBAAkB;AAAA,MAClB,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,SAASF,MAAK,KAAK,WAAW,aAAa;AAAA,IAC7C;AAAA,EACF,CAAC;AAED,gBAAc,cAAc,UAAU;AACtC,aAAW,QAAQ,IAAI;AAEvB,aAAW,YAAY,qBAAqB,CAAC,EAAE,IAAI,MAAM;AACvD,QAAI,IAAI,WAAW,OAAO,KAAK,IAAI,WAAW,QAAQ,GAAG;AACvD,YAAM,aAAa,GAAG;AACtB,aAAO,EAAE,QAAQ,OAAO;AAAA,IAC1B;AACA,WAAO,EAAE,QAAQ,QAAQ;AAAA,EAC3B,CAAC;AAED,MAAI,cAAc,cAAc,GAAG;AACjC,IAAAD,KAAI,KAAK,4BAA4B;AACrC,QAAI;AACF,YAAM,WAAW,QAAQ,uBAAuB;AAAA,IAClD,SAAS,GAAG;AACV,MAAAA,KAAI,MAAM,mEAAmE,CAAC;AAAA,IAChF;AACA,eAAW,YAAY,aAAa;AAAA,EACtC,OAAO;AACL,IAAAA,KAAI,KAAK,oCAAoC,cAAc,cAAc,CAAC,KAAK;AAC/E,UAAM,WAAW,QAAQ,oBAAoB,cAAc,cAAc,CAAC,EAAE;AAAA,EAC9E;AACF;AAEA,QAAQ,OAAO,cAAc,OAAO,OAAO,OAAO,gBAAgB;AAChE,QAAM,MAAMG,eAAc,gBAAgB,MAAM,MAAM;AACtD,MAAI,CAAC,IAAK,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AAE5D,MAAI;AACF,UAAM,EAAE,SAAS,IAAI,MAAMC,QAAO,eAAe,KAAK;AAAA,MACpD,OAAO;AAAA,MACP,aAAa,GAAG,KAAK;AAAA,MACrB,SAAS,CAAC,EAAE,MAAM,aAAa,YAAY,CAAC,KAAK,EAAE,CAAC;AAAA,IACtD,CAAC;AAED,QAAI,CAAC,SAAU,QAAO,EAAE,SAAS,OAAO,UAAU,KAAK;AAEvD,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAEA,UAAM,UAAU,MAAM,YAAY,WAAW;AAC7C,IAAAF,IAAG,cAAc,UAAU,OAAO;AAElC,WAAO,EAAE,SAAS,MAAM,SAAS;AAAA,EACnC,SAAS,OAAY;AACnB,IAAAF,KAAI,MAAM,sBAAsB,KAAK;AACrC,WAAO,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ;AAAA,EAChD;AACF,CAAC;AAED,QAAQ,OAAO,oBAAoB,OAAO,UAAU;AAClD,QAAM,MAAMG,eAAc,gBAAgB,MAAM,MAAM;AACtD,SAAO,eAAe,GAAG;AAC3B,CAAC;AAED,QAAQ,OAAO,2BAA2B,OAAO,OAAO,gBAAwB;AAC9E,QAAM,MAAMA,eAAc,gBAAgB,MAAM,MAAM;AACtD,MAAI,KAAK;AACP,QAAI,mBAAmB,EAAE,YAAY,CAAC;AACtC,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACA,SAAO,EAAE,SAAS,MAAM;AAC1B,CAAC;AAED,QAAQ,OAAO,uBAAuB,YAAY;AAChD,MAAI;AACF,UAAM,OAAO,UAAU,SAAS;AAChC,WAAO,EAAE,SAAS,MAAM,KAAK;AAAA,EAC/B,SAAS,OAAY;AACnB,IAAAH,KAAI,MAAM,0BAA0B,KAAK;AACzC,WAAO,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ;AAAA,EAChD;AACF,CAAC;AAED,QAAQ,OAAO,wBAAwB,OAAO,OAAO,SAAiB;AACpE,MAAI;AACF,cAAU,UAAU,IAAI;AACxB,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,SAAS,OAAY;AACnB,IAAAA,KAAI,MAAM,2BAA2B,KAAK;AAC1C,WAAO,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ;AAAA,EAChD;AACF,CAAC;AAED,eAAe,cAAc;AAC3B,MAAI,cAAc,cAAc,GAAG;AACjC,IAAAA,KAAI,KAAK,qDAAqD;AAC9D;AAAA,EACF;AAEA,QAAM,aAAaC,MAAK,KAAK,WAAW,uBAAuB;AAC/D,QAAM,YAAY,cAAc,UAAU,EAAE;AAE5C,EAAAD,KAAI,KAAK,kCAAkC,UAAU,EAAE;AACvD,MAAI;AACF,UAAM,eAAe,MAAM,OAAO;AAClC,QAAI,aAAa,aAAa;AAC5B,YAAM,OAAO,MAAM,aAAa,YAAY;AAC5C,oBAAc,cAAc,IAAI;AAChC,MAAAA,KAAI,KAAK,gDAAgD,IAAI,EAAE;AAAA,IACjE,OAAO;AACL,MAAAA,KAAI,KAAK,0EAA0E;AAAA,IACrF;AAAA,EACF,SAAS,GAAG;AACV,IAAAA,KAAI,MAAM,oCAAoC,CAAC;AAAA,EACjD;AACF;AAEA,IAAI,UAAU,EAAE,KAAK,YAAY;AAC/B,QAAM,YAAY;AAClB,QAAM,aAAa;AAEnB,wBAAsB;AAEtB,iBAAe,SAAS,4BAA4B,MAAM;AACxD,IAAAA,KAAI,KAAK,2CAA2C;AACpD,UAAM,MAAM,cAAc,cAAc;AACxC,QAAI,KAAK;AACP,UAAI,YAAY,KAAK,iCAAiC;AAAA,IACxD;AAAA,EACF,CAAC;AAED,iBAAe,SAAS,4BAA4B,MAAM;AACxD,IAAAA,KAAI,KAAK,6CAA6C;AACtD,UAAM,MAAM,cAAc,cAAc;AACxC,QAAI,KAAK;AACP,UAAI,YAAY,KAAK,mCAAmC;AAAA,IAC1D;AAAA,EACF,CAAC;AAED,MAAI,GAAG,YAAY,MAAM;AACvB,QAAIG,eAAc,cAAc,EAAE,WAAW,GAAG;AAC9C,mBAAa;AAAA,IACf;AAAA,EACF,CAAC;AACH,CAAC;AAED,IAAI,GAAG,qBAAqB,MAAM;AAChC,iBAAe,cAAc;AAC7B,MAAI,QAAQ,aAAa,UAAU;AACjC,QAAI,KAAK;AAAA,EACX;AACF,CAAC;","names":["BrowserWindow","dialog","path","fs","log","log","path","fs","BrowserWindow","dialog"]} \ No newline at end of file diff --git a/dist-electron/preload.cjs b/dist-electron/preload.cjs new file mode 100644 index 0000000..d2bd941 --- /dev/null +++ b/dist-electron/preload.cjs @@ -0,0 +1,24 @@ +// electron/preload.ts +var import_electron = require("electron"); +console.log("--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---"); +import_electron.contextBridge.exposeInMainWorld("electronAPI", { + exportPDF: (title, htmlContent) => import_electron.ipcRenderer.invoke("export-pdf", title, htmlContent), + selectHtmlFile: () => import_electron.ipcRenderer.invoke("select-html-file"), + updateTitlebarButtons: (symbolColor) => import_electron.ipcRenderer.invoke("update-titlebar-buttons", symbolColor), + onRemoteClipboardSyncToRemote: (callback) => { + import_electron.ipcRenderer.on("remote-clipboard-sync-to-remote", callback); + return () => import_electron.ipcRenderer.removeListener("remote-clipboard-sync-to-remote", callback); + }, + onRemoteClipboardSyncFromRemote: (callback) => { + import_electron.ipcRenderer.on("remote-clipboard-sync-from-remote", callback); + return () => import_electron.ipcRenderer.removeListener("remote-clipboard-sync-from-remote", callback); + }, + onRemoteClipboardAutoSync: (callback) => { + const handler = (_event, text) => callback(text); + import_electron.ipcRenderer.on("remote-clipboard-auto-sync", handler); + return () => import_electron.ipcRenderer.removeListener("remote-clipboard-auto-sync", handler); + }, + clipboardReadText: () => import_electron.ipcRenderer.invoke("clipboard-read-text"), + clipboardWriteText: (text) => import_electron.ipcRenderer.invoke("clipboard-write-text", text) +}); +//# sourceMappingURL=preload.cjs.map \ No newline at end of file diff --git a/dist-electron/preload.cjs.map b/dist-electron/preload.cjs.map new file mode 100644 index 0000000..f069fd9 --- /dev/null +++ b/dist-electron/preload.cjs.map @@ -0,0 +1 @@ +{"version":3,"sources":["../electron/preload.ts"],"sourcesContent":["import { contextBridge, ipcRenderer } from 'electron'\n\nconsole.log('--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---')\n\ncontextBridge.exposeInMainWorld('electronAPI', {\n exportPDF: (title: string, htmlContent?: string) => ipcRenderer.invoke('export-pdf', title, htmlContent),\n selectHtmlFile: () => ipcRenderer.invoke('select-html-file'),\n updateTitlebarButtons: (symbolColor: string) => ipcRenderer.invoke('update-titlebar-buttons', symbolColor),\n onRemoteClipboardSyncToRemote: (callback: () => void) => {\n ipcRenderer.on('remote-clipboard-sync-to-remote', callback);\n return () => ipcRenderer.removeListener('remote-clipboard-sync-to-remote', callback);\n },\n onRemoteClipboardSyncFromRemote: (callback: () => void) => {\n ipcRenderer.on('remote-clipboard-sync-from-remote', callback);\n return () => ipcRenderer.removeListener('remote-clipboard-sync-from-remote', callback);\n },\n onRemoteClipboardAutoSync: (callback: (text: string) => void) => {\n const handler = (_event: Electron.IpcRendererEvent, text: string) => callback(text);\n ipcRenderer.on('remote-clipboard-auto-sync', handler);\n return () => ipcRenderer.removeListener('remote-clipboard-auto-sync', handler);\n },\n clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),\n clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text),\n})\n"],"mappings":";AAAA,sBAA2C;AAE3C,QAAQ,IAAI,4CAA4C;AAExD,8BAAc,kBAAkB,eAAe;AAAA,EAC7C,WAAW,CAAC,OAAe,gBAAyB,4BAAY,OAAO,cAAc,OAAO,WAAW;AAAA,EACvG,gBAAgB,MAAM,4BAAY,OAAO,kBAAkB;AAAA,EAC3D,uBAAuB,CAAC,gBAAwB,4BAAY,OAAO,2BAA2B,WAAW;AAAA,EACzG,+BAA+B,CAAC,aAAyB;AACvD,gCAAY,GAAG,mCAAmC,QAAQ;AAC1D,WAAO,MAAM,4BAAY,eAAe,mCAAmC,QAAQ;AAAA,EACrF;AAAA,EACA,iCAAiC,CAAC,aAAyB;AACzD,gCAAY,GAAG,qCAAqC,QAAQ;AAC5D,WAAO,MAAM,4BAAY,eAAe,qCAAqC,QAAQ;AAAA,EACvF;AAAA,EACA,2BAA2B,CAAC,aAAqC;AAC/D,UAAM,UAAU,CAAC,QAAmC,SAAiB,SAAS,IAAI;AAClF,gCAAY,GAAG,8BAA8B,OAAO;AACpD,WAAO,MAAM,4BAAY,eAAe,8BAA8B,OAAO;AAAA,EAC/E;AAAA,EACA,mBAAmB,MAAM,4BAAY,OAAO,qBAAqB;AAAA,EACjE,oBAAoB,CAAC,SAAiB,4BAAY,OAAO,wBAAwB,IAAI;AACvF,CAAC;","names":[]} \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts new file mode 100644 index 0000000..cbc6534 --- /dev/null +++ b/electron/main.ts @@ -0,0 +1,217 @@ +import { app, BrowserWindow, shell, ipcMain, dialog, nativeTheme, globalShortcut, clipboard } from 'electron'; +import path from 'path'; +import { fileURLToPath, pathToFileURL } from 'url'; +import fs from 'fs'; +import log from 'electron-log'; +import { generatePdf } from './services/pdfGenerator'; +import { selectHtmlFile } from './services/htmlImport'; +import { electronState } from './state'; + +log.initialize(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +process.env.NOTEBOOK_ROOT = path.join(app.getPath('documents'), 'XCNote'); + +if (!fs.existsSync(process.env.NOTEBOOK_ROOT)) { + try { + fs.mkdirSync(process.env.NOTEBOOK_ROOT, { recursive: true }); + } catch (err) { + log.error('Failed to create notebook directory:', err); + } +} + +electronState.setDevelopment(!app.isPackaged); + +let lastClipboardText = ''; + +function startClipboardWatcher() { + lastClipboardText = clipboard.readText(); + + setInterval(() => { + try { + const currentText = clipboard.readText(); + if (currentText && currentText !== lastClipboardText) { + lastClipboardText = currentText; + log.info('Clipboard changed, syncing to remote'); + const win = electronState.getMainWindow(); + if (win) { + win.webContents.send('remote-clipboard-auto-sync', currentText); + } + } + } catch (e) { + // ignore + } + }, 1000); +} + +async function createWindow() { + const initialSymbolColor = nativeTheme.shouldUseDarkColors ? '#ffffff' : '#000000'; + + const mainWindow = new BrowserWindow({ + width: 1280, + height: 800, + minWidth: 1600, + minHeight: 900, + autoHideMenuBar: true, + titleBarStyle: 'hidden', + titleBarOverlay: { + color: '#00000000', + symbolColor: initialSymbolColor, + height: 32, + }, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: false, + webviewTag: true, + preload: path.join(__dirname, 'preload.cjs'), + }, + }); + + electronState.setMainWindow(mainWindow); + mainWindow.setMenu(null); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('http:') || url.startsWith('https:')) { + shell.openExternal(url); + return { action: 'deny' }; + } + return { action: 'allow' }; + }); + + if (electronState.isDevelopment()) { + log.info('Loading development URL...'); + try { + await mainWindow.loadURL('http://localhost:5173'); + } catch (e) { + log.error('Failed to load dev URL. Make sure npm run electron:dev is used.', e); + } + mainWindow.webContents.openDevTools(); + } else { + log.info(`Loading production URL with port ${electronState.getServerPort()}...`); + await mainWindow.loadURL(`http://localhost:${electronState.getServerPort()}`); + } +} + +ipcMain.handle('export-pdf', async (event, title, htmlContent) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) return { success: false, error: 'No window found' }; + + try { + const { filePath } = await dialog.showSaveDialog(win, { + title: '导出 PDF', + defaultPath: `${title}.pdf`, + filters: [{ name: 'PDF Files', extensions: ['pdf'] }] + }); + + if (!filePath) return { success: false, canceled: true }; + + if (!htmlContent) { + throw new Error('No HTML content provided for PDF export'); + } + + const pdfData = await generatePdf(htmlContent); + fs.writeFileSync(filePath, pdfData); + + return { success: true, filePath }; + } catch (error: any) { + log.error('Export PDF failed:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('select-html-file', async (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return selectHtmlFile(win); +}); + +ipcMain.handle('update-titlebar-buttons', async (event, symbolColor: string) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.setTitleBarOverlay({ symbolColor }); + return { success: true }; + } + return { success: false }; +}); + +ipcMain.handle('clipboard-read-text', async () => { + try { + const text = clipboard.readText(); + return { success: true, text }; + } catch (error: any) { + log.error('Clipboard read failed:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('clipboard-write-text', async (event, text: string) => { + try { + clipboard.writeText(text); + return { success: true }; + } catch (error: any) { + log.error('Clipboard write failed:', error); + return { success: false, error: error.message }; + } +}); + +async function startServer() { + if (electronState.isDevelopment()) { + log.info('In dev mode, assuming external servers are running.'); + return; + } + + const serverPath = path.join(__dirname, '../dist-api/server.js'); + const serverUrl = pathToFileURL(serverPath).href; + + log.info(`Starting internal server from: ${serverPath}`); + try { + const serverModule = await import(serverUrl); + if (serverModule.startServer) { + const port = await serverModule.startServer(); + electronState.setServerPort(port); + log.info(`Internal server started successfully on port ${port}`); + } else { + log.warn('startServer function not found in server module, using default port 3001'); + } + } catch (e) { + log.error('Failed to start internal server:', e); + } +} + +app.whenReady().then(async () => { + await startServer(); + await createWindow(); + + startClipboardWatcher(); + + globalShortcut.register('CommandOrControl+Shift+C', () => { + log.info('Global shortcut: sync clipboard to remote'); + const win = electronState.getMainWindow(); + if (win) { + win.webContents.send('remote-clipboard-sync-to-remote'); + } + }); + + globalShortcut.register('CommandOrControl+Shift+V', () => { + log.info('Global shortcut: sync clipboard from remote'); + const win = electronState.getMainWindow(); + if (win) { + win.webContents.send('remote-clipboard-sync-from-remote'); + } + }); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + globalShortcut.unregisterAll(); + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/electron/preload.ts b/electron/preload.ts new file mode 100644 index 0000000..460f0dd --- /dev/null +++ b/electron/preload.ts @@ -0,0 +1,24 @@ +import { contextBridge, ipcRenderer } from 'electron' + +console.log('--- PRELOAD SCRIPT LOADED SUCCESSFULLY ---') + +contextBridge.exposeInMainWorld('electronAPI', { + exportPDF: (title: string, htmlContent?: string) => ipcRenderer.invoke('export-pdf', title, htmlContent), + selectHtmlFile: () => ipcRenderer.invoke('select-html-file'), + updateTitlebarButtons: (symbolColor: string) => ipcRenderer.invoke('update-titlebar-buttons', symbolColor), + onRemoteClipboardSyncToRemote: (callback: () => void) => { + ipcRenderer.on('remote-clipboard-sync-to-remote', callback); + return () => ipcRenderer.removeListener('remote-clipboard-sync-to-remote', callback); + }, + onRemoteClipboardSyncFromRemote: (callback: () => void) => { + ipcRenderer.on('remote-clipboard-sync-from-remote', callback); + return () => ipcRenderer.removeListener('remote-clipboard-sync-from-remote', callback); + }, + onRemoteClipboardAutoSync: (callback: (text: string) => void) => { + const handler = (_event: Electron.IpcRendererEvent, text: string) => callback(text); + ipcRenderer.on('remote-clipboard-auto-sync', handler); + return () => ipcRenderer.removeListener('remote-clipboard-auto-sync', handler); + }, + clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'), + clipboardWriteText: (text: string) => ipcRenderer.invoke('clipboard-write-text', text), +}) diff --git a/electron/server.ts b/electron/server.ts new file mode 100644 index 0000000..7267e2d --- /dev/null +++ b/electron/server.ts @@ -0,0 +1,36 @@ +import app from '../api/app'; +import path from 'path'; +import express from 'express'; +import { fileURLToPath } from 'url'; +import { logger } from '../api/utils/logger'; +import { AddressInfo } from 'net'; +import { startWatcher } from '../api/watcher/watcher.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +startWatcher(); + +const distPath = path.join(__dirname, '../dist'); + +app.use(express.static(distPath)); + +app.get('*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); +}); + +export const startServer = (): Promise => { + return new Promise((resolve, reject) => { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + const port = address.port; + logger.info(`Electron internal server running on port ${port}`); + resolve(port); + }); + + server.on('error', (err) => { + logger.error('Failed to start server:', err); + reject(err); + }); + }); +}; diff --git a/electron/services/htmlImport.ts b/electron/services/htmlImport.ts new file mode 100644 index 0000000..18a7141 --- /dev/null +++ b/electron/services/htmlImport.ts @@ -0,0 +1,69 @@ +import { dialog, BrowserWindow } from 'electron' +import path from 'path' +import fs from 'fs' +import log from 'electron-log' + +export interface HtmlImportResult { + success: boolean + canceled?: boolean + error?: string + htmlPath?: string + htmlDir?: string + htmlFileName?: string + assetsDirName?: string + assetsFiles?: string[] +} + +export const selectHtmlFile = async (win: BrowserWindow | null): Promise => { + if (!win) return { success: false, error: 'No window found' } + + try { + const { filePaths, canceled } = await dialog.showOpenDialog(win, { + title: '选择 HTML 文件', + filters: [ + { name: 'HTML Files', extensions: ['html', 'htm'] } + ], + properties: ['openFile'] + }) + + if (canceled || filePaths.length === 0) { + return { success: false, canceled: true } + } + + const htmlPath = filePaths[0] + const htmlDir = path.dirname(htmlPath) + const htmlFileName = path.basename(htmlPath, path.extname(htmlPath)) + + const assetsDirName = `${htmlFileName}_files` + const assetsDirPath = path.join(htmlDir, assetsDirName) + + const assetsFiles: string[] = [] + if (fs.existsSync(assetsDirPath)) { + const collectFiles = (dir: string, baseDir: string) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + collectFiles(fullPath, baseDir) + } else { + const relPath = path.relative(baseDir, fullPath) + assetsFiles.push(relPath) + } + } + } + collectFiles(assetsDirPath, assetsDirPath) + } + + return { + success: true, + htmlPath, + htmlDir, + htmlFileName, + assetsDirName, + assetsFiles + } + } catch (error: any) { + log.error('Select HTML file failed:', error) + return { success: false, error: error.message } + } +} diff --git a/electron/services/pdfGenerator.ts b/electron/services/pdfGenerator.ts new file mode 100644 index 0000000..9a16e4f --- /dev/null +++ b/electron/services/pdfGenerator.ts @@ -0,0 +1,46 @@ +import { BrowserWindow } from 'electron'; + +/** + * 生成 PDF 的服务 + * @param htmlContent 完整的 HTML 字符串 + * @returns PDF 文件的二进制数据 + */ +export async function generatePdf(htmlContent: string): Promise { + const printWin = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: false, // 与 main.ts 保持一致,确保脚本执行权限 + } + }); + + try { + await printWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`); + + // 等待资源加载完成 (由 generatePrintHtml 注入的脚本控制) + await printWin.webContents.executeJavaScript(` + new Promise(resolve => { + const check = () => { + if (window.__PRINT_READY__) { + resolve(); + } else { + setTimeout(check, 100); + } + } + check(); + }) + `); + + const pdfData = await printWin.webContents.printToPDF({ + printBackground: true, + pageSize: 'A4', + margins: { top: 0, bottom: 0, left: 0, right: 0 } + }); + + return pdfData; + } finally { + // 确保窗口被关闭,防止内存泄漏 + printWin.close(); + } +} diff --git a/electron/state.ts b/electron/state.ts new file mode 100644 index 0000000..57f69d3 --- /dev/null +++ b/electron/state.ts @@ -0,0 +1,49 @@ +import { BrowserWindow } from 'electron' + +interface ElectronAppState { + mainWindow: BrowserWindow | null + serverPort: number + isDev: boolean +} + +class ElectronState { + private state: ElectronAppState = { + mainWindow: null, + serverPort: 3001, + isDev: false, + } + + getMainWindow(): BrowserWindow | null { + return this.state.mainWindow + } + + setMainWindow(window: BrowserWindow | null): void { + this.state.mainWindow = window + } + + getServerPort(): number { + return this.state.serverPort + } + + setServerPort(port: number): void { + this.state.serverPort = port + } + + isDevelopment(): boolean { + return this.state.isDev + } + + setDevelopment(isDev: boolean): void { + this.state.isDev = isDev + } + + reset(): void { + this.state = { + mainWindow: null, + serverPort: 3001, + isDev: false, + } + } +} + +export const electronState = new ElectronState() diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0bd6962 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + }, + }, +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..956eba6 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + + XCNote + + + +
+ + + diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..86022e1 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,10 @@ +{ + "watch": ["api"], + "ext": "ts,mts,js,json", + "ignore": ["api/dist/*"], + "exec": "tsx api/server.ts", + "env": { + "NODE_ENV": "development" + }, + "delay": 1000 +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6d0b538 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,16074 @@ +{ + "name": "xcnote", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xcnote", + "version": "0.0.0", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@milkdown/core": "^7.18.0", + "@milkdown/plugin-block": "^7.18.0", + "@milkdown/plugin-history": "^7.18.0", + "@milkdown/plugin-listener": "^7.18.0", + "@milkdown/plugin-math": "^7.5.9", + "@milkdown/plugin-prism": "^7.18.0", + "@milkdown/preset-commonmark": "^7.18.0", + "@milkdown/preset-gfm": "^7.18.0", + "@milkdown/react": "^7.18.0", + "@types/jszip": "^3.4.0", + "@types/multer": "^2.0.0", + "@types/prismjs": "^1.26.5", + "axios": "^1.13.5", + "chokidar": "^5.0.0", + "clsx": "^2.1.1", + "cors": "^2.8.5", + "dotenv": "^17.2.1", + "electron-log": "^5.4.3", + "express": "^4.21.2", + "github-slugger": "^2.0.0", + "jszip": "^3.10.1", + "lucide-react": "^0.511.0", + "multer": "^2.0.2", + "prismjs": "^1.30.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.3.0", + "recharts": "^3.7.0", + "remark-breaks": "^4.0.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@tailwindcss/typography": "^0.5.19", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/cors": "^2.8.19", + "@types/express": "^4.17.21", + "@types/node": "^22.19.13", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vercel/node": "^5.3.6", + "@vitejs/plugin-react": "^4.4.1", + "@vitest/ui": "^4.0.18", + "autoprefixer": "^10.4.21", + "babel-plugin-react-dev-locator": "^1.0.0", + "concurrently": "^9.2.0", + "electron": "^40.2.1", + "electron-builder": "^26.7.0", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "happy-dom": "^20.7.0", + "jsdom": "^28.1.0", + "nodemon": "^3.1.10", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "tsup": "^8.5.1", + "tsx": "^4.20.3", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.18", + "wait-on": "^9.0.3" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz", + "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", + "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-jinja": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.0.tgz", + "integrity": "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.1.tgz", + "integrity": "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", + "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-jinja": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz", + "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.12", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.12.tgz", + "integrity": "sha512-f+/VsHVn/kOA9lltk/GFzuYwVVAKmOnNjxbrhkk3tPHntFqjWeI2TbIXx006YkBkqC10wZ4NsnWXCQiFPeAISQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.29.tgz", + "integrity": "sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@develar/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@edge-runtime/format": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@edge-runtime/format/-/format-2.2.1.tgz", + "integrity": "sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/node-utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/node-utils/-/node-utils-2.3.0.tgz", + "integrity": "sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/ponyfill": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@edge-runtime/ponyfill/-/ponyfill-2.4.2.tgz", + "integrity": "sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/primitives": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.1.0.tgz", + "integrity": "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/vm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.2.0.tgz", + "integrity": "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@edge-runtime/primitives": "4.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@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/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz", + "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/go": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", + "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@milkdown/components": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/components/-/components-7.18.0.tgz", + "integrity": "sha512-Zu/GMqy1byyxul/+/RWcpe02b7luhtW1SfTYNFZnaWPvIap5M9vG7pFeQNRqJe5cbfKI+bvW8Ubyb5BG2kb9Ug==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/exception": "7.18.0", + "@milkdown/plugin-tooltip": "7.18.0", + "@milkdown/preset-commonmark": "7.18.0", + "@milkdown/preset-gfm": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/transformer": "7.18.0", + "@milkdown/utils": "7.18.0", + "@types/lodash-es": "^4.17.12", + "clsx": "^2.0.0", + "dompurify": "^3.2.5", + "lodash-es": "^4.17.21", + "nanoid": "^5.0.9", + "unist-util-visit": "^5.0.0", + "vue": "^3.5.20" + }, + "peerDependencies": { + "@codemirror/language": "^6", + "@codemirror/state": "^6", + "@codemirror/view": "^6" + } + }, + "node_modules/@milkdown/components/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@milkdown/core": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/core/-/core-7.18.0.tgz", + "integrity": "sha512-BUVR/72XwrtM3qHTTtXtmCtGfuaAexvSxosYIXw7d6ElbLiLIe3bOXjGwwgLHW3xsq23VKmYMsFqWLUFt6uGDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@milkdown/ctx": "7.18.0", + "@milkdown/exception": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/transformer": "7.18.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.3" + } + }, + "node_modules/@milkdown/crepe": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/crepe/-/crepe-7.18.0.tgz", + "integrity": "sha512-GcHW6Use0MCRvFg6RQVN5EaeyMlxFxDEGbGwqApnBblxZi5PV9nlAAn0AfOhYvFHSDkQ3rQa5fuHQ0Bd0KobQQ==", + "license": "MIT", + "dependencies": { + "@codemirror/commands": "^6.2.4", + "@codemirror/language": "^6.10.1", + "@codemirror/language-data": "^6.3.1", + "@codemirror/state": "^6.4.1", + "@codemirror/theme-one-dark": "^6.1.2", + "@codemirror/view": "^6.16.0", + "@milkdown/kit": "7.18.0", + "@types/lodash-es": "^4.17.12", + "clsx": "^2.0.0", + "codemirror": "^6.0.1", + "katex": "^0.16.0", + "lodash-es": "^4.17.21", + "prosemirror-virtual-cursor": "^0.4.2", + "remark-math": "^6.0.0", + "unist-util-visit": "^5.0.0", + "vue": "^3.5.20" + } + }, + "node_modules/@milkdown/ctx": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/ctx/-/ctx-7.18.0.tgz", + "integrity": "sha512-F+t8U/akpY7Vw+KD+z32Itr6lrVLAGTVO79DN436BnFK/J9kiPzTRfTet6fMOj3NlwO/24lUluiPZd7qbCmn8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@milkdown/exception": "7.18.0" + } + }, + "node_modules/@milkdown/exception": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/exception/-/exception-7.18.0.tgz", + "integrity": "sha512-sAyi4IqdChh4+lpgucmgDZNGjYuIRvJimZeMj0SdfdeHDABan5Nco3X+5yOGaBq1z9QOJG90+vEcEvUASHBmFw==", + "license": "MIT" + }, + "node_modules/@milkdown/kit": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/kit/-/kit-7.18.0.tgz", + "integrity": "sha512-6C8c/bU+3Md/rlZFTqMmdVen2xSC80LYBOZ/G4+W39gsV7x/ux/HRdd8xk75a4IrHKgq6EJpGJ1yH8BvT7P+1A==", + "license": "MIT", + "dependencies": { + "@milkdown/components": "7.18.0", + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/plugin-block": "7.18.0", + "@milkdown/plugin-clipboard": "7.18.0", + "@milkdown/plugin-cursor": "7.18.0", + "@milkdown/plugin-history": "7.18.0", + "@milkdown/plugin-indent": "7.18.0", + "@milkdown/plugin-listener": "7.18.0", + "@milkdown/plugin-slash": "7.18.0", + "@milkdown/plugin-tooltip": "7.18.0", + "@milkdown/plugin-trailing": "7.18.0", + "@milkdown/plugin-upload": "7.18.0", + "@milkdown/preset-commonmark": "7.18.0", + "@milkdown/preset-gfm": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/transformer": "7.18.0", + "@milkdown/utils": "7.18.0" + } + }, + "node_modules/@milkdown/plugin-block": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-block/-/plugin-block-7.18.0.tgz", + "integrity": "sha512-+x00o7Vh5nQesw4j6QwtwCThdjSiH/jUvAzrTpwr8xvRmQnmztdfdJhPHxp48pK/sIEct3660HWuwDpdeAlmRw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-clipboard": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-clipboard/-/plugin-clipboard-7.18.0.tgz", + "integrity": "sha512-Gnp+GqkoLS1pKG9S2QfdvZQjfoJosQek5Yv5zOIj5X388yfVlguKNtCwnDCJKVEVws9e8PnhfPBmzr06713dZw==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0" + } + }, + "node_modules/@milkdown/plugin-cursor": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-cursor/-/plugin-cursor-7.18.0.tgz", + "integrity": "sha512-SsvFEeFMv1jrzVBnuAMyAwZzhjwCk4wmGjJEug41Ic+CT0YMUtVPJn5QVn7fjixR13kzkfaNDUPZ+sGNqIR2xw==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0", + "prosemirror-drop-indicator": "^0.1.0" + } + }, + "node_modules/@milkdown/plugin-history": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-history/-/plugin-history-7.18.0.tgz", + "integrity": "sha512-hWM3rpad/THy267dXgEWRu9Arf+3j2KE8UN3jhqsUvVLZZ2ZetaPc2imHowJaLR8PwCb649+1RxL+IKrXizNKQ==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0" + } + }, + "node_modules/@milkdown/plugin-indent": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-indent/-/plugin-indent-7.18.0.tgz", + "integrity": "sha512-LAVMSsy6lWvy/QjvSazojUeW6v1lLFj5Fjv3YvqDNtP6/RSOIhHJs75aXbv92Kx43aRJnkh7EVy9Wu4OxSC70Q==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0" + } + }, + "node_modules/@milkdown/plugin-listener": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-listener/-/plugin-listener-7.18.0.tgz", + "integrity": "sha512-F2iPKdWYGJX5kMnmIeZeybQ5gZUwT/smNBbt/itPBn5cD4YRF1qmY/MxDs0+nvoN2NSxtEx5pHOtd5/E4mCf2A==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-math": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-math/-/plugin-math-7.5.9.tgz", + "integrity": "sha512-LzFZKNc7zm3uOTs9mEN/12/X+UdGoyQRp5Gt9GE7lPJ96jOZaGJD0vMtX0mTauTOmUxDQoQS0I7NW1g9UhC6qQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "@milkdown/exception": "7.5.9", + "@milkdown/utils": "7.5.9", + "@types/katex": "^0.16.0", + "katex": "^0.16.0", + "remark-math": "^6.0.0", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@milkdown/core": "^7.2.0", + "@milkdown/ctx": "^7.2.0", + "@milkdown/prose": "^7.2.0" + } + }, + "node_modules/@milkdown/plugin-math/node_modules/@milkdown/exception": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/@milkdown/exception/-/exception-7.5.9.tgz", + "integrity": "sha512-9g+WpiRjgLsVlHt7DotlUmKK9oT6Lsr5TgxE0NdDvtr81CC43mgNtoekI6rg/PatEBBXifDK1GJJ4LKnRSseVQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@milkdown/plugin-math/node_modules/@milkdown/utils": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/@milkdown/utils/-/utils-7.5.9.tgz", + "integrity": "sha512-udxaIdKm6pOiNACIKR7+NQ7Vj+QivhA5jcRqksEuQOfDFQBFcGaAhGEEiU1cKwE7fmP2ayigfD0IZ01rjLS2fA==", + "license": "MIT", + "dependencies": { + "@milkdown/exception": "7.5.9", + "nanoid": "^5.0.0", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@milkdown/core": "^7.2.0", + "@milkdown/ctx": "^7.2.0", + "@milkdown/prose": "^7.2.0", + "@milkdown/transformer": "^7.2.0" + } + }, + "node_modules/@milkdown/plugin-math/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@milkdown/plugin-prism": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-prism/-/plugin-prism-7.18.0.tgz", + "integrity": "sha512-uF/6UW67wCIbkK6V+StT9ETlGFwSO3VEiLsW2JzBdU3XMwPGr4q3ldPoG9pojXD8jE/yYjDd033wY3ICvPP1gA==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0", + "@types/hast": "^3.0.4", + "refractor": "^5.0.0" + } + }, + "node_modules/@milkdown/plugin-slash": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-slash/-/plugin-slash-7.18.0.tgz", + "integrity": "sha512-jBcaLswX1yKG97s0V1qFqk/0aR+LpWnTCHIrryNVRIRFYm7B6tITekkqwALlV2bqE1eykeN2j8yEyRQ63Wv05Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-tooltip": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-tooltip/-/plugin-tooltip-7.18.0.tgz", + "integrity": "sha512-Z8WYSEFANhHPS2A8uMIcKGJ3vt0KKCJ80hffuJffudJT9FSIXieh1f8OKcKQuhcRHxRCRUApMcOOjOptiVaHvQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-trailing": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-trailing/-/plugin-trailing-7.18.0.tgz", + "integrity": "sha512-AusCWoZSRfgsStdlmg+4sYZ08HLDDiHhesDCqiLCdo1bklNhzK/9q6gxdL1HP5xTn5a4xV9hUrI7E7M0JaKdug==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0" + } + }, + "node_modules/@milkdown/plugin-upload": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-upload/-/plugin-upload-7.18.0.tgz", + "integrity": "sha512-fsWwd6g6FX35Wg12KVE1Yu3wU8vM5hA567DufeHcik9LckdLJcZKf35JMJDUOAOkEdU3V91BKO47KUhBPFt1jA==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/exception": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/utils": "7.18.0" + } + }, + "node_modules/@milkdown/preset-commonmark": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/preset-commonmark/-/preset-commonmark-7.18.0.tgz", + "integrity": "sha512-L/F9vmhQKOjKJZTEEsKjDu/2KkMTDxBVQISk4w+j8KFWx9OpHBwqWqyHiDLTREbT7pJqLfyB96eXvfuMG4za5g==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/exception": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/transformer": "7.18.0", + "@milkdown/utils": "7.18.0", + "remark-inline-links": "^7.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@milkdown/preset-gfm": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/preset-gfm/-/preset-gfm-7.18.0.tgz", + "integrity": "sha512-NLfkd7HOaaMCMImXmBh8TX8KNkgKecM7YRHFEwb5D/SMLyBLyZs7lDfLEKPU9N52+vzgwMz8ceUSlCElmneTJg==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/exception": "7.18.0", + "@milkdown/preset-commonmark": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/transformer": "7.18.0", + "@milkdown/utils": "7.18.0", + "prosemirror-safari-ime-span": "^1.0.1", + "remark-gfm": "^4.0.1" + } + }, + "node_modules/@milkdown/prose": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/prose/-/prose-7.18.0.tgz", + "integrity": "sha512-bRDfgVM6uKRaejvju/FWdQMryQc4kSSso+fnABUbvbCKitXnsgRPvclsddbt3J92anQwLRDWr/qotx1NcyDM1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@milkdown/exception": "7.18.0", + "prosemirror-changeset": "^2.3.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.0", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.1", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.3" + } + }, + "node_modules/@milkdown/react": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/react/-/react-7.18.0.tgz", + "integrity": "sha512-hk7CN6YqhazUBOdY0Iyh3RjvRyjsl2vBsJyf54ua38hxmaAD13KbTnEWZs30OnryoP6cv9z74bHPMIc2UnSVIQ==", + "license": "MIT", + "dependencies": { + "@milkdown/crepe": "7.18.0", + "@milkdown/kit": "7.18.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@milkdown/transformer": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/transformer/-/transformer-7.18.0.tgz", + "integrity": "sha512-AzTgqDktQw9nzgrpICjYNxScYwwnxmALPSyZ39Y0wNZJafi8QMVqLv4w2bhyYkxITXolPHdLAAsZXPKuMjrmNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@milkdown/exception": "7.18.0", + "@milkdown/prose": "7.18.0", + "remark": "^15.0.1", + "unified": "^11.0.3" + } + }, + "node_modules/@milkdown/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@milkdown/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-+o/1sky+QwbS0Y92HthTupMFziJKhZUgF7IBS55Ft4Wjt63kX8PHaLC9KtewNawpzyM/CjPJ9ySCIa+C/06Bsg==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.18.0", + "@milkdown/ctx": "7.18.0", + "@milkdown/exception": "7.18.0", + "@milkdown/prose": "7.18.0", + "@milkdown/transformer": "7.18.0", + "nanoid": "^5.0.9" + } + }, + "node_modules/@milkdown/utils/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ocavue/utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ocavue/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-hB6uTz58shfG/ZyrBkMjub2YyJ1ue0vE14zRLZWJl/zEcgMBFTX6nBBV6ncyRHK4qOIBwFNpXzZrjTFj1ofxRA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ocavue" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@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/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@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/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@ts-morph/common": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", + "integrity": "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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://registry.npmjs.org/@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/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@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/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@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/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jszip": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz", + "integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==", + "license": "MIT", + "dependencies": { + "jszip": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vercel/build-utils": { + "version": "13.2.17", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.2.17.tgz", + "integrity": "sha512-uEf3xGE573ig4WV1uPuJKsUUQXX9bX0zghV0p/hNtG5xrvQSJpf1Vaw/Z9p2k+FzWRZEy1wCOsb5BquR8Agecw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/error-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.0.3.tgz", + "integrity": "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/nft": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.1.1.tgz", + "integrity": "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^13.0.0", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@vercel/node": { + "version": "5.5.29", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.5.29.tgz", + "integrity": "sha512-U7pn/8w6d/02oVEn34qEC4/EOUwv7R07W1slkB+xgou/shrKSEmJaX/S3YydE6OeGRayNLMvI0fv8SIFezqYjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@edge-runtime/node-utils": "2.3.0", + "@edge-runtime/primitives": "4.1.0", + "@edge-runtime/vm": "3.2.0", + "@types/node": "20.11.0", + "@vercel/build-utils": "13.2.17", + "@vercel/error-utils": "2.0.3", + "@vercel/nft": "1.1.1", + "@vercel/static-config": "3.1.2", + "async-listen": "3.0.0", + "cjs-module-lexer": "1.2.3", + "edge-runtime": "2.5.9", + "es-module-lexer": "1.4.1", + "esbuild": "0.27.0", + "etag": "1.8.1", + "mime-types": "2.1.35", + "node-fetch": "2.6.9", + "path-to-regexp": "6.1.0", + "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", + "ts-morph": "12.0.0", + "ts-node": "10.9.1", + "typescript": "4.9.5", + "typescript5": "npm:typescript@5.9.3", + "undici": "5.28.4" + } + }, + "node_modules/@vercel/node/node_modules/@types/node": { + "version": "20.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vercel/node/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@vercel/node/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/static-config": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.1.2.tgz", + "integrity": "sha512-2d+TXr6K30w86a+WbMbGm2W91O0UzO5VeemZYBBUJbCjk/5FLLGIi8aV6RS2+WmaRvtcqNTn2pUA7nCOK3bGcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "8.6.3", + "json-schema-to-ts": "1.6.4", + "ts-morph": "12.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "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": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.0.18", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz", + "integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.6.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.7.0", + "electron-builder-squirrel-windows": "26.7.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "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": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/async-listen": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz", + "integrity": "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-react-dev-locator": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/babel-plugin-react-dev-locator/-/babel-plugin-react-dev-locator-1.0.6.tgz", + "integrity": "sha512-XWi+6x6e4NvVwvOqwitci/BZU1xbNfNuL94kfT4kWfP/d9p3RYRVOrogs1Z1otlYfQUO07cy/20z8eY9blFSpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", + "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/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/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "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/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/code-block-writer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", + "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-hrtime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-3.0.0.tgz", + "integrity": "sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "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://registry.npmjs.org/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-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dmg-builder": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz", + "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.7.0", + "builder-util": "26.4.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-license/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/dmg-license/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/edge-runtime": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz", + "integrity": "sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@edge-runtime/format": "2.2.1", + "@edge-runtime/ponyfill": "2.4.2", + "@edge-runtime/vm": "3.2.0", + "async-listen": "3.0.1", + "mri": "1.2.0", + "picocolors": "1.0.0", + "pretty-ms": "7.0.1", + "signal-exit": "4.0.2", + "time-span": "4.0.0" + }, + "bin": { + "edge-runtime": "dist/cli/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/edge-runtime/node_modules/async-listen": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.1.tgz", + "integrity": "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/edge-runtime/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "40.2.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.2.1.tgz", + "integrity": "sha512-0zOeyN8LB1KHIjVV5jyMmQmkqx3J8OkkVlab3p7vOM28jI46blxW7M52Tcdi6X2m5o2jj8ejOlAh5+boL3w8aQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.7.0.tgz", + "integrity": "sha512-LoXbCvSFxLesPneQ/fM7FB4OheIDA2tjqCdUkKlObV5ZKGhYgi5VHPHO/6UUOUodAlg7SrkPx7BZJPby+Vrtbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.7.0", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.7.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.7.0.tgz", + "integrity": "sha512-3EqkQK+q0kGshdPSKEPb2p5F75TENMKu6Fe5aTdeaPfdzFK4Yjp5L0d6S7K8iyvqIsGQ/ei4bnpyX9wt+kVCKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.7.0", + "builder-util": "26.4.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-log": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", + "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/electron-publish": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", + "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.10.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz", + "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/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/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz", + "integrity": "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/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/happy-dom": { + "version": "20.7.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.7.0.tgz", + "integrity": "sha512-hR/uLYQdngTyEfxnOoa+e6KTcfBFyc1hgFj/Cc144A5JJUuHFYqIEBDcD4FeGqUeKLRZqJ9eN9u7/GDjYEgS1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/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/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-to-ts": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-1.6.4.tgz", + "integrity": "sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.6", + "ts-toolbelt": "^6.15.5" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/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": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/katex": { + "version": "0.16.28", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz", + "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/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/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/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.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", + "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp-updated": { + "name": "path-to-regexp", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-drop-indicator": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/prosemirror-drop-indicator/-/prosemirror-drop-indicator-0.1.3.tgz", + "integrity": "sha512-fJV6G2tHIVXZLUuc60fS9ly1/GuGOlAZUm67S1El+kGFUYh27Hyv6hcGx3rrJ+Q/JZL5jnyAibIZYYWpPqE45g==", + "license": "MIT", + "dependencies": { + "@ocavue/utils": "^1.0.0", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-view": "^1.41.3" + }, + "funding": { + "url": "https://github.com/sponsors/ocavue" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "peer": true, + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-safari-ime-span": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/prosemirror-safari-ime-span/-/prosemirror-safari-ime-span-1.0.2.tgz", + "integrity": "sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.4.3", + "prosemirror-view": "^1.33.8" + }, + "funding": { + "url": "https://github.com/sponsors/ocavue" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", + "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", + "license": "MIT", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/prosemirror-virtual-cursor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/prosemirror-virtual-cursor/-/prosemirror-virtual-cursor-0.4.2.tgz", + "integrity": "sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ocavue" + }, + "peerDependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + }, + "peerDependenciesMeta": { + "prosemirror-model": { + "optional": true + }, + "prosemirror-state": { + "optional": true + }, + "prosemirror-view": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-inline-links": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/remark-inline-links/-/remark-inline-links-7.0.0.tgz", + "integrity": "sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-definitions": "^6.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/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.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/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/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/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/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/time-span": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-4.0.0.tgz", + "integrity": "sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-hrtime": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ts-morph": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-12.0.0.tgz", + "integrity": "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.11.0", + "code-block-writer": "^10.1.1" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tsup/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tsup/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript5": { + "name": "typescript", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/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/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "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-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@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/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "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/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wait-on": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz", + "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.13.2", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e59ec87 --- /dev/null +++ b/package.json @@ -0,0 +1,138 @@ +{ + "name": "xcnote", + "version": "0.0.0", + "description": "一个功能强大的本地 Markdown 笔记管理工具,支持时间追踪、任务管理、AI 集成等高级功能", + "keywords": [ + "markdown", + "note", + "笔记", + "electron", + "react", + "typescript", + "时间追踪", + "任务管理" + ], + "author": "Your Name", + "repository": { + "type": "git", + "url": "https://github.com/your-username/xcnote.git" + }, + "private": true, + "main": "dist-electron/main.js", + "type": "module", + "scripts": { + "client:dev": "vite", + "build": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.api.json && vite build", + "lint": "eslint .", + "preview": "vite preview", + "check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.api.json", + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest --coverage", + "test:run": "vitest run", + "server:dev": "nodemon", + "dev": "concurrently \"npm run client:dev\" \"npm run server:dev\"", + "watch:electron": "tsup --config tsup.electron.ts --watch", + "electron:dev": "concurrently -k \"npm run server:dev\" \"npm run client:dev\" \"wait-on tcp:3001 tcp:5173 && npm run watch:electron\"", + "build:electron": "tsup --config tsup.electron.ts", + "build:api": "tsup electron/server.ts --format esm --out-dir dist-api --clean --no-splitting --target esnext", + "electron:build": "npm run build && npm run build:electron && npm run build:api && electron-builder" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@milkdown/core": "^7.18.0", + "@milkdown/plugin-block": "^7.18.0", + "@milkdown/plugin-history": "^7.18.0", + "@milkdown/plugin-listener": "^7.18.0", + "@milkdown/plugin-math": "^7.5.9", + "@milkdown/plugin-prism": "^7.18.0", + "@milkdown/preset-commonmark": "^7.18.0", + "@milkdown/preset-gfm": "^7.18.0", + "@milkdown/react": "^7.18.0", + "@types/jszip": "^3.4.0", + "@types/multer": "^2.0.0", + "@types/prismjs": "^1.26.5", + "axios": "^1.13.5", + "chokidar": "^5.0.0", + "clsx": "^2.1.1", + "cors": "^2.8.5", + "dotenv": "^17.2.1", + "electron-log": "^5.4.3", + "express": "^4.21.2", + "github-slugger": "^2.0.0", + "jszip": "^3.10.1", + "lucide-react": "^0.511.0", + "multer": "^2.0.2", + "prismjs": "^1.30.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.3.0", + "recharts": "^3.7.0", + "remark-breaks": "^4.0.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@tailwindcss/typography": "^0.5.19", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/cors": "^2.8.19", + "@types/express": "^4.17.21", + "@types/node": "^22.19.13", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vercel/node": "^5.3.6", + "@vitejs/plugin-react": "^4.4.1", + "@vitest/ui": "^4.0.18", + "autoprefixer": "^10.4.21", + "babel-plugin-react-dev-locator": "^1.0.0", + "concurrently": "^9.2.0", + "electron": "^40.2.1", + "electron-builder": "^26.7.0", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "happy-dom": "^20.7.0", + "jsdom": "^28.1.0", + "nodemon": "^3.1.10", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "tsup": "^8.5.1", + "tsx": "^4.20.3", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.18", + "wait-on": "^9.0.3" + }, + "build": { + "appId": "com.xcnote.app", + "productName": "XCNote", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "dist-electron/**/*", + "dist-api/**/*", + "shared/**/*", + "tools/**/*", + "package.json" + ], + "asarUnpack": [ + "tools/**/*" + ], + "win": { + "target": "nsis" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..1d8a859 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,10 @@ +/** WARNING: DON'T EDIT THIS FILE */ +/** WARNING: DON'T EDIT THIS FILE */ +/** WARNING: DON'T EDIT THIS FILE */ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/background.png b/public/background.png new file mode 100644 index 0000000..430263e Binary files /dev/null and b/public/background.png differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..c04c3c1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/remote/.gitignore b/remote/.gitignore new file mode 100644 index 0000000..a3ae974 --- /dev/null +++ b/remote/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +logs/ +.env +config/custom.json +*.log +.DS_Store +Thumbs.db diff --git a/remote/.trae/documents/ring-notification-plan.md b/remote/.trae/documents/ring-notification-plan.md new file mode 100644 index 0000000..7604047 --- /dev/null +++ b/remote/.trae/documents/ring-notification-plan.md @@ -0,0 +1,367 @@ +# Ring 通知功能实现计划 + +## 一、功能概述 + +### 1.1 使用场景 + +远程电脑执行长链任务(如 opencode、trae 等工具)时,通过 `ring.py` 脚本向主控电脑发送通知,提醒用户任务完成。 + +### 1.2 调用方式 + +```bash +python ring.py "任务完成" +python ring.py "编译成功" --title "Build" +python ring.py "下载完成" --sound +``` + +### 1.3 架构设计 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 主控电脑 (被控端) │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ Ring Server (端口 3002) ││ +│ │ - HTTP POST /ring ││ +│ │ - 接收通知请求 ││ +│ │ - 触发系统通知 (Windows Toast) ││ +│ │ - 可选:播放提示音 ││ +│ └─────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────┘ + ▲ + │ HTTP POST + │ +┌─────────────────────────────┴───────────────────────────────────┐ +│ 远程电脑 (控制端) │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ ring.py ││ +│ │ - 发送 HTTP 请求到主控电脑 ││ +│ │ - 支持自定义消息、标题、提示音 ││ +│ └─────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、实现步骤 + +### 步骤 1:创建 RingService 服务 + +**文件:** `src/services/ring/RingService.js` + +**功能:** +- 使用 Windows Toast 通知 API +- 支持自定义标题和消息 +- 可选播放提示音 + +**实现方式:** +- 使用 PowerShell 调用 Windows Toast 通知 +- 或使用 `node-notifier` 库 + +### 步骤 2:创建 Ring Server + +**文件:** `src/server/RingServer.js` + +**功能:** +- 独立的 HTTP 服务器,监听端口 3002 +- 提供 `/ring` POST 接口 +- 调用 RingService 发送通知 + +**API 设计:** +``` +POST /ring +Content-Type: application/json + +{ + "message": "任务完成", + "title": "Ring", // 可选,默认 "Ring" + "sound": true // 可选,默认 false +} +``` + +### 步骤 3:集成到 App.js + +**修改文件:** `src/core/App.js` + +**修改内容:** +- 注册 RingService +- 注册 RingServer +- 启动时同时启动 Ring Server + +### 步骤 4:更新配置 + +**修改文件:** `src/config/schema.js` + +**新增配置:** +```javascript +ring: { + port: { type: 'number', default: 3002 }, + enabled: { type: 'boolean', default: true } +} +``` + +### 步骤 5:创建 ring.py 客户端脚本 + +**文件:** `scripts/ring.py` + +**功能:** +- 命令行参数解析 +- 发送 HTTP 请求到主控电脑 +- 支持配置目标地址 + +**使用方式:** +```bash +python ring.py "消息内容" +python ring.py "消息" --title "标题" --sound +python ring.py "消息" --host 192.168.1.100 --port 3002 +``` + +--- + +## 三、文件清单 + +### 新增文件 + +| 文件路径 | 说明 | +|---------|------| +| `src/services/ring/RingService.js` | 通知服务,调用系统通知 | +| `src/services/ring/index.js` | 服务导出 | +| `src/server/RingServer.js` | 独立 HTTP 服务器 | +| `scripts/ring.py` | Python 客户端脚本 | + +### 修改文件 + +| 文件路径 | 修改内容 | +|---------|---------| +| `src/core/App.js` | 注册并启动 Ring 服务 | +| `src/config/schema.js` | 添加 ring 配置项 | +| `config/default.json` | 添加 ring 默认配置 | + +--- + +## 四、详细实现 + +### 4.1 RingService.js + +```javascript +const { spawn } = require('child_process'); +const logger = require('../../utils/logger'); + +class RingService { + async notify({ message, title = 'Ring', sound = false }) { + // 使用 PowerShell 发送 Windows Toast 通知 + const psScript = ` + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null + [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null + + $template = @" + + + + ${title} + ${message} + + + ${sound ? ' +"@ + + $xml = New-Object Windows.Data.Xml.Dom.XmlDocument + $xml.LoadXml($template) + $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) + [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Ring").Show($toast) + `; + + return new Promise((resolve, reject) => { + const ps = spawn('powershell', ['-NoProfile', '-Command', psScript]); + ps.on('close', (code) => { + if (code === 0) { + logger.info('Ring notification sent', { title, message }); + resolve(true); + } else { + resolve(false); + } + }); + ps.on('error', (err) => { + logger.error('Ring notification failed', { error: err.message }); + resolve(false); + }); + }); + } +} + +module.exports = RingService; +``` + +### 4.2 RingServer.js + +```javascript +const http = require('http'); +const logger = require('../utils/logger'); + +class RingServer { + constructor(config = {}) { + this.port = config.port || 3002; + this.host = config.host || '0.0.0.0'; + this.server = null; + this.ringService = config.ringService; + } + + start() { + this.server = http.createServer((req, res) => { + if (req.method === 'POST' && req.url === '/ring') { + this.handleRing(req, res); + } else { + res.writeHead(404); + res.end('Not Found'); + } + }); + + return new Promise((resolve, reject) => { + this.server.listen(this.port, this.host, () => { + logger.info('Ring server started', { port: this.port }); + resolve({ port: this.port, host: this.host }); + }); + this.server.on('error', reject); + }); + } + + async handleRing(req, res) { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', async () => { + try { + const data = JSON.parse(body); + await this.ringService.notify(data); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true })); + } catch (err) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + }); + } + + stop() { + return new Promise((resolve) => { + if (this.server) { + this.server.close(() => { + logger.info('Ring server stopped'); + resolve(); + }); + } else { + resolve(); + } + }); + } +} + +module.exports = RingServer; +``` + +### 4.3 ring.py + +```python +#!/usr/bin/env python3 +import argparse +import requests +import sys + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 3002 + +def main(): + parser = argparse.ArgumentParser(description='Send notification to remote computer') + parser.add_argument('message', help='Notification message') + parser.add_argument('--title', '-t', default='Ring', help='Notification title') + parser.add_argument('--sound', '-s', action='store_true', help='Play notification sound') + parser.add_argument('--host', default=DEFAULT_HOST, help='Target host') + parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Target port') + + args = parser.parse_args() + + try: + response = requests.post( + f"http://{args.host}:{args.port}/ring", + json={ + "message": args.message, + "title": args.title, + "sound": args.sound + }, + timeout=5 + ) + if response.status_code == 200: + print("Notification sent successfully") + else: + print(f"Failed to send notification: {response.text}") + sys.exit(1) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() +``` + +--- + +## 五、使用示例 + +### 5.1 基本使用 + +```bash +# 发送简单通知 +python ring.py "任务完成" + +# 自定义标题 +python ring.py "编译成功" --title "Build" + +# 带提示音 +python ring.py "下载完成" --sound + +# 指定目标主机 +python ring.py "远程任务完成" --host 192.168.1.100 --port 3002 +``` + +### 5.2 在脚本中使用 + +```bash +# 长时间任务完成后通知 +npm run build && python ring.py "Build completed" --sound + +# 或者在脚本中 +long_running_command +python ring.py "Command finished: $?" +``` + +--- + +## 六、配置说明 + +### config/default.json + +```json +{ + "ring": { + "enabled": true, + "port": 3002 + } +} +``` + +--- + +## 七、安全考虑 + +1. **内网使用** - Ring Server 默认只监听内网,不暴露到公网 +2. **可选认证** - 后续可添加简单 token 认证 +3. **频率限制** - 防止通知轰炸 + +--- + +## 八、测试计划 + +1. 启动应用,验证 Ring Server 在 3002 端口启动 +2. 使用 curl 测试:`curl -X POST http://localhost:3002/ring -H "Content-Type: application/json" -d '{"message":"test"}'` +3. 验证 Windows 通知弹出 +4. 测试 ring.py 脚本 +5. 测试远程主机调用 diff --git a/remote/README.md b/remote/README.md new file mode 100644 index 0000000..01fc2b4 --- /dev/null +++ b/remote/README.md @@ -0,0 +1,587 @@ +# 远程屏幕监控系统 + +一个基于 Node.js 的实时远程屏幕监控和控制系统,支持鼠标和键盘的远程操作、剪贴板同步、文件传输等功能。 + +## 功能特点 + +- 🎥 **实时屏幕流** - 低延迟的屏幕视频流传输,使用 FFmpeg 进行 MPEG1 编码 +- 🖱️ **鼠标控制** - 远程鼠标移动、点击和滚轮操作 +- ⌨️ **键盘控制** - 远程键盘输入,支持特殊键和组合键 +- 📋 **剪贴板同步** - 支持文本和图片的双向剪贴板同步 +- **文件传输** - 支持大文件分块上传、下载和文件管理 +- 🔒 **安全认证** - bcrypt 密码哈希 + JWT Token 认证 +- 🌐 **内网穿透** - 集成 FRP 客户端,支持外网访问 +- 📦 **Git 服务** - 可选集成 Gitea,提供代码托管服务 +- 📝 **日志系统** - 完整的运行日志记录 +- ⚙️ **灵活配置** - 支持配置文件和环境变量配置 + +## 快速开始 + +### 环境要求 + +- Node.js >= 16.0.0 +- Windows 操作系统(输入控制功能需要) +- FFmpeg(已内置安装) + +### 安装依赖 + +```bash +npm install +``` + +### 启动服务 + +```bash +npm run dev +# 或 +npm start +``` + +访问 http://localhost:3000 查看屏幕流。 + +## 配置 + +配置文件位于 `config/default.json`,可配置项包括: + +```json +{ + "server": { + "port": 3000, + "host": "0.0.0.0" + }, + "stream": { + "fps": 30, + "bitrate": "4000k", + "gop": 10, + "preset": "ultrafast", + "resolution": { + "width": 1920, + "height": 1080 + } + }, + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + }, + "security": { + "password": "", + "tokenExpiry": 3600 + }, + "frp": { + "enabled": true, + "frpcPath": "./frp/frpc.exe", + "configPath": "./frp/frpc.toml" + }, + "gitea": { + "enabled": true + } +} +``` + +### 配置说明 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| server.port | 服务器端口 | 3000 | +| server.host | 服务器监听地址 | 0.0.0.0 | +| stream.fps | 视频帧率 | 30 | +| stream.bitrate | 视频码率 | 4000k | +| stream.gop | GOP 大小(关键帧间隔) | 10 | +| stream.preset | 编码预设 | ultrafast | +| stream.resolution | 视频分辨率 | 1920x1080 | +| input.mouseEnabled | 是否启用鼠标控制 | true | +| input.keyboardEnabled | 是否启用键盘控制 | true | +| input.sensitivity | 鼠标灵敏度 | 1.0 | +| security.password | 访问密码(空表示不需要密码) | "" | +| security.tokenExpiry | Token 有效期(秒) | 3600 | +| frp.enabled | 是否启用 FRP 内网穿透 | true | +| gitea.enabled | 是否启用 Gitea 服务 | true | + +### 环境变量配置 + +所有配置项都可以通过环境变量覆盖,格式为 `REMOTE_
_`: + +```bash +# 设置服务器端口 +REMOTE_SERVER_PORT=8080 + +# 设置密码 +REMOTE_SECURITY_PASSWORD=your_password + +# 设置 JWT 密钥 +JWT_SECRET=your_jwt_secret +``` + +## API 接口 + +### 认证接口 + +#### 登录 + +```bash +POST /login +Content-Type: application/json + +{ + "password": "your_password" +} +``` + +响应:设置 `auth` Cookie 并重定向到首页。 + +#### API 登录 + +```bash +POST /api/auth/login +Content-Type: application/json + +{ + "password": "your_password" +} +``` + +响应: +```json +{ + "success": true, + "token": "jwt_token_here" +} +``` + +#### 验证 Token + +```bash +POST /api/auth/verify +Authorization: Bearer +``` + +响应: +```json +{ + "success": true, + "valid": true, + "userId": "default-user" +} +``` + +### 鼠标控制 + +```bash +POST /api/input/mouse/move +Content-Type: application/json + +{ + "x": 100, + "y": 200 +} +``` + +```bash +POST /api/input/mouse/down +Content-Type: application/json + +{ + "button": "left" # "left", "right" 或 "middle" +} +``` + +```bash +POST /api/input/mouse/up +Content-Type: application/json + +{ + "button": "left" +} +``` + +```bash +POST /api/input/mouse/click +Content-Type: application/json + +{ + "button": "left" +} +``` + +```bash +POST /api/input/mouse/wheel +Content-Type: application/json + +{ + "delta": 120 # 正值向上滚动,负值向下滚动 +} +``` + +### 键盘控制 + +```bash +POST /api/input/keyboard/down +Content-Type: application/json + +{ + "key": "enter" +} +``` + +```bash +POST /api/input/keyboard/up +Content-Type: application/json + +{ + "key": "enter" +} +``` + +```bash +POST /api/input/keyboard/press +Content-Type: application/json + +{ + "key": "enter" +} +``` + +```bash +POST /api/input/keyboard/type +Content-Type: application/json + +{ + "text": "Hello World" +} +``` + +支持的特殊键:`enter`, `backspace`, `tab`, `escape`, `delete`, `home`, `end`, `pageup`, `pagedown`, `up`, `down`, `left`, `right`, `f1`-`f12`, `ctrl`, `alt`, `shift`, `win`, `space` + +### 流媒体接口 + +```bash +GET /api/stream/info +``` + +响应: +```json +{ + "success": true, + "stream": { + "status": "running", + "resolution": { + "width": 1920, + "height": 1080 + }, + "fps": 30, + "bitrate": "4000k", + "gop": 10, + "encoder": "mpeg1video" + } +} +``` + +```bash +POST /api/stream/start +``` + +```bash +POST /api/stream/stop +``` + +### 文件传输接口 + +```bash +GET /api/files +``` + +响应: +```json +{ + "files": [ + { + "name": "example.txt", + "size": 1024, + "modified": "2026-03-05T10:00:00.000Z", + "type": ".txt" + } + ] +} +``` + +```bash +GET /api/files/browse?path=relative/path +``` + +响应: +```json +{ + "items": [ + { + "name": "folder", + "isDirectory": true, + "size": 0, + "modified": "2026-03-05T10:00:00.000Z", + "type": "directory" + } + ], + "currentPath": "relative/path", + "parentPath": "" +} +``` + +```bash +POST /api/files/upload/start +Content-Type: application/json + +{ + "filename": "large_file.zip", + "totalChunks": 10, + "fileSize": 50000000 +} +``` + +响应: +```json +{ + "fileId": "abc123", + "chunkSize": 5242880, + "message": "Upload session started" +} +``` + +```bash +POST /api/files/upload/chunk +Content-Type: multipart/form-data + +fileId: abc123 +chunkIndex: 0 +chunk: +``` + +```bash +POST /api/files/upload/merge +Content-Type: application/json + +{ + "fileId": "abc123", + "totalChunks": 10, + "filename": "large_file.zip" +} +``` + +```bash +GET /api/files/:filename +``` + +支持 Range 请求头进行断点续传。 + +```bash +DELETE /api/files/:filename +``` + +### WebSocket 消息类型 + +连接地址:`ws://localhost:3000/ws` + +#### 客户端发送 + +| 类型 | 说明 | 数据 | +|------|------|------| +| mouseMove | 鼠标移动 | `{ type: "mouseMove", x: 100, y: 200 }` | +| mouseDown | 鼠标按下 | `{ type: "mouseDown", button: "left" }` | +| mouseUp | 鼠标释放 | `{ type: "mouseUp", button: "left" }` | +| mouseWheel | 鼠标滚轮 | `{ type: "mouseWheel", delta: 120 }` | +| keyDown | 键盘按下 | `{ type: "keyDown", key: "enter" }` | +| keyUp | 键盘释放 | `{ type: "keyUp", key: "enter" }` | +| clipboardGet | 获取剪贴板 | `{ type: "clipboardGet" }` | +| clipboardSet | 设置剪贴板 | `{ type: "clipboardSet", contentType: "text", data: "content" }` | + +#### 服务端发送 + +| 类型 | 说明 | 数据 | +|------|------|------| +| screenInfo | 屏幕信息 | `{ type: "screenInfo", width: 1920, height: 1080 }` | +| clipboardData | 剪贴板数据 | `{ type: "clipboardData", contentType: "text", data: "content", size: 100 }` | +| clipboardResult | 剪贴板操作结果 | `{ type: "clipboardResult", success: true }` | +| clipboardTooLarge | 剪贴板内容过大 | `{ type: "clipboardTooLarge", size: 1000000 }` | + +## 项目结构 + +``` +remote/ +├── src/ +│ ├── config/ # 配置管理 +│ │ ├── index.js # 配置加载器 +│ │ └── schema.js # 配置验证模式 +│ ├── controllers/ # 控制器层 +│ │ ├── AuthController.js # 认证控制器 +│ │ ├── InputController.js# 输入控制器 +│ │ └── StreamController.js# 流媒体控制器 +│ ├── core/ # 核心模块 +│ │ ├── App.js # 应用主类 +│ │ ├── Container.js # 依赖注入容器 +│ │ ├── EventBus.js # 事件总线 +│ │ ├── ErrorHandler.js # 错误处理器 +│ │ └── events.js # 事件类型定义 +│ ├── middlewares/ # 中间件 +│ │ ├── auth.js # 认证中间件 +│ │ ├── error.js # 错误处理中间件 +│ │ └── rateLimit.js # 限流中间件 +│ ├── routes/ # 路由层 +│ │ ├── index.js # 路由汇总 +│ │ ├── auth.js # 认证路由 +│ │ ├── files.js # 文件路由 +│ │ ├── input.js # 输入路由 +│ │ └── stream.js # 流媒体路由 +│ ├── server/ # 服务器层 +│ │ ├── Server.js # HTTP 服务器 +│ │ ├── WebSocketServer.js# WebSocket 服务器 +│ │ ├── StreamBroadcaster.js# 流广播器 +│ │ ├── InputHandler.js # 输入处理器 +│ │ └── messageTypes.js # 消息类型定义 +│ ├── services/ # 服务层 +│ │ ├── auth/ # 认证服务 +│ │ │ ├── AuthService.js +│ │ │ └── TokenManager.js +│ │ ├── clipboard/ # 剪贴板服务 +│ │ │ └── ClipboardService.js +│ │ ├── file/ # 文件服务 +│ │ │ └── FileService.js +│ │ ├── input/ # 输入服务 +│ │ │ ├── InputService.js +│ │ │ └── PowerShellInput.js +│ │ ├── network/ # 网络服务 +│ │ │ ├── FRPService.js +│ │ │ └── GiteaService.js +│ │ ├── stream/ # 流媒体服务 +│ │ │ ├── FFmpegEncoder.js +│ │ │ ├── ScreenCapture.js +│ │ │ └── StreamService.js +│ │ └── index.js # 服务汇总 +│ ├── utils/ # 工具类 +│ │ ├── config.js # 配置工具 +│ │ ├── logger.js # 日志工具 +│ │ └── paths.js # 路径工具 +│ └── index.js # 应用入口 +├── config/ +│ └── default.json # 默认配置 +├── docs/ # 文档目录 +│ ├── 开发/ # 开发文档 +│ └── 指南/ # 使用指南 +├── frp/ # FRP 内网穿透 +│ ├── frpc.exe +│ ├── frpc.toml +│ └── frpc-runtime.toml +├── gitea/ # Gitea Git 服务 +├── logs/ # 日志目录 +│ ├── combined.log # 所有日志 +│ └── error.log # 错误日志 +├── public/ # 前端静态文件 +│ ├── css/ +│ │ └── main.css +│ ├── js/ +│ │ ├── app.js # 应用入口 +│ │ ├── file-panel.js # 文件传输面板 +│ │ ├── input.js # 输入处理 +│ │ ├── jsmpeg.min.js # JSMpeg 播放器 +│ │ ├── player.js # 视频播放器 +│ │ └── utils.js # 工具函数 +│ └── index.html +├── scripts/ +│ └── migrate-password.js # 密码迁移脚本 +├── uploads/ # 上传文件目录 +├── .gitignore +├── package.json +└── README.md +``` + +## 构建与部署 + +### 开发环境 + +```bash +npm run dev +``` + +### 生产环境打包 + +使用 pkg 打包为可执行文件: + +```bash +npm run build +``` + +打包后的文件位于 `dist/` 目录: +- `remote-screen-monitor.exe` - 主程序 +- `public/` - 前端静态文件 +- `config/` - 配置文件 +- `frp/` - FRP 客户端 +- `ffmpeg.exe` - FFmpeg 编码器 + +### Windows 服务安装 + +使用 NSSM 将应用安装为 Windows 服务: + +```powershell +# 安装服务 +nssm install RemoteApp "C:\path\to\remote-screen-monitor.exe" + +# 设置工作目录 +nssm set RemoteApp AppDirectory "C:\path\to\app" + +# 设置启动类型 +nssm set RemoteApp Start SERVICE_AUTO_START + +# 启动服务 +nssm start RemoteApp +``` + +详细说明请参考 [NSSM使用指南](docs/指南/NSSM使用指南.md)。 + +## 安全注意事项 + +⚠️ **重要提示**: +- 在公共网络上使用时,务必设置密码保护 +- 建议使用 HTTPS/WSS(通过反向代理如 Nginx) +- 不要使用 root/Administrator 权限运行此服务 +- 定期更换密码和 JWT 密钥 + +### 密码安全 + +系统支持 bcrypt 密码哈希。使用迁移脚本生成安全的密码哈希: + +```bash +node scripts/migrate-password.js +``` + +将生成的哈希值设置到环境变量 `REMOTE_SECURITY_PASSWORD` 中。 + +## 故障排除 + +### 鼠标/键盘控制不工作 + +确保以管理员权限运行(Windows),某些操作可能需要提升的权限。 + +### 视频流卡顿 + +- 降低 FPS 或分辨率 +- 降低视频码率 +- 检查网络带宽 + +### 日志查看 + +日志文件位于 `logs/` 目录: +- `logs/combined.log` - 所有日志 +- `logs/error.log` - 错误日志 + +## 技术栈 + +- **后端**: Node.js, Express 5.x, ws (WebSocket), winston (日志) +- **前端**: HTML5 Canvas, JSMpeg 播放器 +- **视频编码**: FFmpeg (mpeg1video) +- **输入模拟**: PowerShell + Windows API (user32.dll) +- **认证**: bcrypt, jsonwebtoken +- **文件处理**: multer, fs-extra +- **内网穿透**: FRP (Fast Reverse Proxy) +- **打包**: pkg + +## 许可证 + +ISC diff --git a/remote/config/default.json b/remote/config/default.json new file mode 100644 index 0000000..508ab1c --- /dev/null +++ b/remote/config/default.json @@ -0,0 +1,33 @@ +{ + "server": { + "port": 3000, + "host": "0.0.0.0" + }, + "stream": { + "fps": 30, + "bitrate": "4000k", + "gop": 10, + "preset": "ultrafast", + "resolution": { + "width": 1920, + "height": 1080 + } + }, + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + }, + "security": { + "password": "wzw20040525", + "tokenExpiry": 3600 + }, + "frp": { + "enabled": true, + "frpcPath": "./frp/frpc.exe", + "configPath": "./frp/frpc.toml" + }, + "gitea": { + "enabled": true + } +} diff --git a/remote/docs/开发/API文档.md b/remote/docs/开发/API文档.md new file mode 100644 index 0000000..4781c76 --- /dev/null +++ b/remote/docs/开发/API文档.md @@ -0,0 +1,784 @@ +# API 文档 + +## 目录 + +- [认证接口](#认证接口) +- [输入控制接口](#输入控制接口) +- [流媒体接口](#流媒体接口) +- [文件传输接口](#文件传输接口) +- [WebSocket 消息类型](#websocket-消息类型) + +--- + +## 认证接口 + +### POST /login + +Web 登录接口,用于用户身份认证。 + +**请求体** + +```json +{ + "password": "your_password" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| password | string | 否 | 用户密码(如果服务器未配置密码则不需要) | + +**响应示例** + +成功响应(200): +```json +{ + "success": true, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "message": "Authentication disabled" +} +``` + +失败响应(400): +```json +{ + "success": false, + "error": "Password is required" +} +``` + +失败响应(401): +```json +{ + "success": false, + "error": "Invalid password" +} +``` + +--- + +### POST /api/auth/login + +API 登录接口,功能与 `/login` 相同。 + +**请求体** + +```json +{ + "password": "your_password" +} +``` + +**响应示例** + +成功响应(200): +```json +{ + "success": true, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +--- + +### POST /api/auth/verify + +Token 验证接口,用于验证 JWT Token 是否有效。 + +**请求头** + +``` +Authorization: Bearer +``` + +**响应示例** + +成功响应(200): +```json +{ + "success": true, + "valid": true, + "userId": "default-user" +} +``` + +失败响应(401): +```json +{ + "success": false, + "valid": false, + "error": "No token provided" +} +``` + +--- + +## 输入控制接口 + +所有输入控制接口需要在请求头中携带有效的 Token: + +``` +Authorization: Bearer +``` + +### 鼠标操作 + +#### POST /api/input/mouse/move + +移动鼠标到指定坐标。 + +**请求体** + +```json +{ + "x": 100, + "y": 200 +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| x | number | 是 | 目标 X 坐标 | +| y | number | 是 | 目标 Y 坐标 | + +**响应示例** + +成功响应(200): +```json +{ + "success": true +} +``` + +失败响应(400): +```json +{ + "success": false, + "error": "Invalid coordinates" +} +``` + +--- + +#### POST /api/input/mouse/down + +按下鼠标按键。 + +**请求体** + +```json +{ + "button": "left" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| button | string | 否 | left | 鼠标按键:`left`、`right`、`middle` | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +#### POST /api/input/mouse/up + +释放鼠标按键。 + +**请求体** + +```json +{ + "button": "left" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| button | string | 否 | left | 鼠标按键:`left`、`right`、`middle` | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +#### POST /api/input/mouse/click + +点击鼠标按键。 + +**请求体** + +```json +{ + "button": "left" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| button | string | 否 | left | 鼠标按键:`left`、`right`、`middle` | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +#### POST /api/input/mouse/wheel + +鼠标滚轮滚动。 + +**请求体** + +```json +{ + "delta": 100 +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| delta | number | 是 | 滚动量(正数向上,负数向下) | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +### 键盘操作 + +#### POST /api/input/keyboard/down + +按下键盘按键。 + +**请求体** + +```json +{ + "key": "a" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| key | string | 是 | 按键名称(如 `a`、`enter`、`ctrl`、`shift` 等) | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +#### POST /api/input/keyboard/up + +释放键盘按键。 + +**请求体** + +```json +{ + "key": "a" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| key | string | 是 | 按键名称 | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +#### POST /api/input/keyboard/press + +按下并释放键盘按键(单击)。 + +**请求体** + +```json +{ + "key": "enter" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| key | string | 是 | 按键名称 | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +#### POST /api/input/keyboard/type + +输入文本字符串。 + +**请求体** + +```json +{ + "text": "Hello World" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| text | string | 是 | 要输入的文本内容 | + +**响应示例** + +```json +{ + "success": true +} +``` + +--- + +## 流媒体接口 + +### GET /api/stream/info + +获取流媒体信息。 + +**响应示例** + +```json +{ + "success": true, + "stream": { + "status": "running", + "resolution": { + "width": 1920, + "height": 1080 + }, + "fps": 30, + "bitrate": 2000, + "gop": 60, + "encoder": "libx264" + } +} +``` + +**字段说明** + +| 字段 | 类型 | 说明 | +|------|------|------| +| status | string | 流状态:`running` 或 `stopped` | +| resolution | object | 屏幕分辨率 | +| fps | number | 帧率 | +| bitrate | number | 比特率(kbps) | +| gop | number | GOP 大小 | +| encoder | string | 编码器名称 | + +--- + +### POST /api/stream/start + +启动流媒体。 + +**响应示例** + +```json +{ + "success": true, + "message": "Stream started" +} +``` + +已运行时响应: +```json +{ + "success": true, + "message": "Stream is already running" +} +``` + +--- + +### POST /api/stream/stop + +停止流媒体。 + +**响应示例** + +```json +{ + "success": true, + "message": "Stream stopped" +} +``` + +未运行时响应: +```json +{ + "success": true, + "message": "Stream is not running" +} +``` + +--- + +## 文件传输接口 + +### GET /api/files + +获取文件列表。 + +**响应示例** + +```json +{ + "files": [ + { + "name": "example.txt", + "size": 1024, + "modifiedTime": "2024-01-01T12:00:00.000Z" + } + ] +} +``` + +--- + +### GET /api/files/browse + +浏览目录内容。 + +**查询参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| path | string | 否 | 目录路径(默认为根目录) | + +**请求示例** + +``` +GET /api/files/browse?path=documents +``` + +**响应示例** + +```json +{ + "path": "documents", + "directories": ["folder1", "folder2"], + "files": [ + { + "name": "file1.txt", + "size": 1024, + "modifiedTime": "2024-01-01T12:00:00.000Z" + } + ] +} +``` + +--- + +### POST /api/files/upload/start + +开始上传会话(分块上传第一步)。 + +**请求体** + +```json +{ + "filename": "example.zip", + "totalChunks": 10, + "fileSize": 52428800 +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| filename | string | 是 | 文件名 | +| totalChunks | number | 是 | 总分块数 | +| fileSize | number | 是 | 文件总大小(字节) | + +**响应示例** + +```json +{ + "fileId": "a1b2c3d4e5f6g7h8", + "chunkSize": 5242880, + "message": "Upload session started" +} +``` + +--- + +### POST /api/files/upload/chunk + +上传文件分块(分块上传第二步)。 + +**请求体** + +Content-Type: `multipart/form-data` + +| 字段 | 类型 | 说明 | +|------|------|------| +| fileId | string | 上传会话 ID | +| chunkIndex | number | 分块索引(从 0 开始) | +| chunk | file | 分块数据 | + +**响应示例** + +```json +{ + "success": true, + "chunkIndex": 0 +} +``` + +--- + +### POST /api/files/upload/merge + +合并文件分块(分块上传第三步)。 + +**请求体** + +```json +{ + "fileId": "a1b2c3d4e5f6g7h8", + "totalChunks": 10, + "filename": "example.zip" +} +``` + +**参数说明** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| fileId | string | 是 | 上传会话 ID | +| totalChunks | number | 是 | 总分块数 | +| filename | string | 是 | 最终文件名 | + +**响应示例** + +```json +{ + "success": true, + "filename": "example.zip" +} +``` + +--- + +### GET /api/files/:filename + +下载文件。 + +**路径参数** + +| 参数 | 类型 | 说明 | +|------|------|------| +| filename | string | 文件名 | + +**请求头** + +支持 Range 请求,可用于断点续传: + +``` +Range: bytes=0-1023 +``` + +**响应示例** + +成功响应(200 或 206): +- Content-Type: `application/octet-stream` +- Accept-Ranges: `bytes` +- Content-Length: 文件大小 +- Content-Range: 范围(仅 206 响应) + +失败响应(404): +```json +{ + "error": "File not found" +} +``` + +--- + +### DELETE /api/files/:filename + +删除文件。 + +**路径参数** + +| 参数 | 类型 | 说明 | +|------|------|------| +| filename | string | 文件名 | + +**响应示例** + +成功响应(200): +```json +{ + "success": true +} +``` + +失败响应(404): +```json +{ + "error": "File not found" +} +``` + +--- + +## WebSocket 消息类型 + +### 客户端发送的消息类型 + +| 消息类型 | 说明 | 数据格式 | +|----------|------|----------| +| `mouseMove` | 鼠标移动 | `{ x: number, y: number }` | +| `mouseDown` | 鼠标按下 | `{ button: 'left' \| 'right' \| 'middle' }` | +| `mouseUp` | 鼠标释放 | `{ button: 'left' \| 'right' \| 'middle' }` | +| `mouseWheel` | 鼠标滚轮 | `{ delta: number }` | +| `keyDown` | 键盘按下 | `{ key: string }` | +| `keyUp` | 键盘释放 | `{ key: string }` | +| `clipboardGet` | 获取剪贴板内容 | 无额外数据 | +| `clipboardSet` | 设置剪贴板内容 | `{ content: string }` | + +### 服务端发送的消息类型 + +| 消息类型 | 说明 | 数据格式 | +|----------|------|----------| +| `screenInfo` | 屏幕信息 | `{ width: number, height: number }` | +| `clipboardData` | 剪贴板数据 | `{ content: string }` | +| `clipboardResult` | 剪贴板操作结果 | `{ success: boolean }` | +| `clipboardTooLarge` | 剪贴板数据过大 | `{ message: string }` | + +### 消息格式示例 + +**客户端发送鼠标移动:** +```json +{ + "type": "mouseMove", + "data": { + "x": 100, + "y": 200 + } +} +``` + +**客户端发送键盘按键:** +```json +{ + "type": "keyDown", + "data": { + "key": "ctrl" + } +} +``` + +**服务端发送屏幕信息:** +```json +{ + "type": "screenInfo", + "data": { + "width": 1920, + "height": 1080 + } +} +``` + +**客户端请求剪贴板:** +```json +{ + "type": "clipboardGet" +} +``` + +**服务端返回剪贴板数据:** +```json +{ + "type": "clipboardData", + "data": { + "content": "复制的文本内容" + } +} +``` + +--- + +## 错误响应格式 + +所有接口在发生错误时返回统一的错误格式: + +```json +{ + "success": false, + "error": "错误描述信息" +} +``` + +常见 HTTP 状态码: + +| 状态码 | 说明 | +|--------|------| +| 200 | 请求成功 | +| 400 | 请求参数错误 | +| 401 | 未授权(Token 无效或过期) | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | diff --git a/remote/docs/开发/Electron迁移计划.md b/remote/docs/开发/Electron迁移计划.md new file mode 100644 index 0000000..0c2cdd1 --- /dev/null +++ b/remote/docs/开发/Electron迁移计划.md @@ -0,0 +1,382 @@ +# Electron 客户端迁移计划 + +## 项目概述 + +将现有的 `remote-screen-monitor` Web 应用迁移为 Electron 桌面客户端,解决浏览器安全限制导致的剪贴板和文件上传功能受限问题。 + +## 当前问题分析 + +### 浏览器安全限制 + +| 功能 | 当前状态 | 限制原因 | +|------|---------|---------| +| 剪贴板读取 | ❌ 受限 | `navigator.clipboard.readText()` 需要安全上下文 (HTTPS/localhost) | +| 剪贴板写入 | ❌ 受限 | 同上 | +| 文件选择 | ⚠️ 部分可用 | 只能通过 `` 选择,无法直接访问文件系统 | +| 文件下载 | ⚠️ 部分可用 | 只能通过 Blob 下载,无法指定保存位置 | +| 目录浏览 | ❌ 不可用 | File System Access API 需要安全上下文 | + +### 受影响代码位置 + +- `public/index.html` - 剪贴板同步逻辑 (第 47-51, 93-97 行) +- `public/js/file-panel.js` - 文件传输面板 + +--- + +## 迁移方案 + +### 架构设计 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Electron 主进程 (Main Process) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ 剪贴板服务 │ │ 文件服务 │ │ 窗口管理 & 生命周期 │ │ +│ │ - readText │ │ - 浏览目录 │ │ - BrowserWindow │ │ +│ │ - writeText │ │ - 选择文件 │ │ - 托盘图标 │ │ +│ │ - readImage │ │ - 保存文件 │ │ - 系统菜单 │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ +│ │ │ +│ IPC Handler │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + IPC (进程间通信) + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ Preload Script (预加载脚本) │ +│ - contextBridge.exposeInMainWorld │ +│ - 暴露安全的 API 给渲染进程 │ +│ - 验证和清理输入参数 │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + window.electronAPI + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ 渲染进程 (Renderer Process) │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ 现有 Web 应用 (public/) ││ +│ │ - index.html ││ +│ │ - js/app.js, player.js, input.js, file-panel.js ││ +│ │ - 通过 window.electronAPI 调用主进程功能 ││ +│ └─────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 详细任务清单 + +### 阶段一:项目初始化 (预计 30 分钟) + +#### 1.1 安装 Electron 依赖 + +```bash +npm install electron --save-dev +npm install electron-builder --save-dev +``` + +#### 1.2 创建 Electron 目录结构 + +``` +remote/ +├── electron/ +│ ├── main.js # 主进程入口 +│ ├── preload.js # 预加载脚本 +│ ├── ipc/ +│ │ ├── clipboard.js # 剪贴板 IPC 处理 +│ │ └── file.js # 文件系统 IPC 处理 +│ └── utils/ +│ └── paths.js # 路径工具函数 +├── public/ # 现有前端代码 (保持不变) +├── src/ # 现有后端代码 (保持不变) +└── package.json # 更新配置 +``` + +#### 1.3 更新 package.json + +```json +{ + "main": "electron/main.js", + "scripts": { + "electron:dev": "electron .", + "electron:build": "electron-builder", + "electron:build:win": "electron-builder --win --x64" + } +} +``` + +--- + +### 阶段二:主进程开发 (预计 1 小时) + +#### 2.1 创建主进程入口 (electron/main.js) + +**功能需求:** +- 创建 BrowserWindow +- 加载远程服务器 URL 或本地 HTML +- 配置 webPreferences (contextIsolation, nodeIntegration) +- 注册 IPC Handler +- 创建系统托盘图标 +- 处理窗口生命周期 + +**关键配置:** +```javascript +const win = new BrowserWindow({ + webPreferences: { + nodeIntegration: false, // 安全:禁用 Node.js 集成 + contextIsolation: true, // 安全:启用上下文隔离 + preload: path.join(__dirname, 'preload.js') + } +}) + +// 加载远程服务器 +win.loadURL('http://192.168.x.x:3000') +// 或加载本地文件 (离线模式) +// win.loadFile('public/index.html') +``` + +#### 2.2 创建预加载脚本 (electron/preload.js) + +**功能需求:** +- 使用 contextBridge 暴露安全 API +- 封装 IPC 调用 +- 提供类型安全的接口 + +**暴露的 API:** +```javascript +window.electronAPI = { + // 剪贴板 + clipboard: { + readText: () => Promise, + writeText: (text: string) => Promise, + readImage: () => Promise, // base64 + writeImage: (base64: string) => Promise + }, + + // 文件系统 + file: { + showOpenDialog: (options) => Promise, + showSaveDialog: (options) => Promise, + browseDirectory: (path: string) => Promise, + readFile: (path: string) => Promise, + writeFile: (path: string, data: Buffer) => Promise + }, + + // 平台信息 + platform: { + isElectron: true, + os: 'win32' | 'darwin' | 'linux' + } +} +``` + +#### 2.3 实现剪贴板 IPC 处理 (electron/ipc/clipboard.js) + +**功能需求:** +- 使用 Electron 原生 `clipboard` 模块 +- 支持文本和图片格式 +- 替代现有的 PowerShell 方案 + +**优势:** +- 无需启动 PowerShell 进程 +- 更快的响应速度 +- 更好的跨平台支持 + +#### 2.4 实现文件系统 IPC 处理 (electron/ipc/file.js) + +**功能需求:** +- 使用 Electron `dialog` 模块显示文件选择对话框 +- 支持本地目录浏览 +- 支持直接文件读写 + +--- + +### 阶段三:渲染进程适配 (预计 1 小时) + +#### 3.1 创建适配层 (public/js/electron-adapter.js) + +**功能需求:** +- 检测运行环境 (Electron vs Browser) +- 统一 API 接口 +- 自动选择最优实现 + +**设计模式:** +```javascript +const ClipboardAPI = { + async readText() { + if (window.electronAPI) { + return window.electronAPI.clipboard.readText() + } + // 回退到浏览器 API + return navigator.clipboard.readText() + } +} +``` + +#### 3.2 修改剪贴板同步逻辑 + +**文件:** `public/index.html` + +**修改内容:** +- 替换 `navigator.clipboard` 调用为适配层 API +- 添加 Electron 环境检测 +- 保留浏览器环境兼容性 + +#### 3.3 增强文件面板功能 + +**文件:** `public/js/file-panel.js` + +**新增功能:** +- 本地目录浏览 (Electron 环境) +- 拖拽上传支持 +- 右键菜单 (删除、重命名) +- 文件预览 + +--- + +### 阶段四:构建配置 (预计 30 分钟) + +#### 4.1 配置 electron-builder + +**package.json 配置:** +```json +{ + "build": { + "appId": "com.xuanchi.remote-screen", + "productName": "XC Remote", + "directories": { + "output": "dist-electron" + }, + "files": [ + "electron/**/*", + "public/**/*" + ], + "win": { + "target": [ + { "target": "nsis", "arch": ["x64"] }, + { "target": "portable", "arch": ["x64"] } + ], + "icon": "public/icon.ico" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } + } +} +``` + +#### 4.2 创建安装程序图标 + +- 需要 .ico 格式图标文件 +- 建议尺寸:256x256 + +--- + +### 阶段五:测试与优化 (预计 1 小时) + +#### 5.1 功能测试清单 + +- [ ] 剪贴板文本同步 (Ctrl+C / Ctrl+V) +- [ ] 剪贴板图片同步 +- [ ] 文件上传 +- [ ] 文件下载 +- [ ] 目录浏览 +- [ ] 远程屏幕显示 +- [ ] 鼠标键盘控制 + +#### 5.2 性能优化 + +- 剪贴板轮询优化 (避免频繁读取) +- 文件传输进度优化 +- 内存使用监控 + +#### 5.3 安全检查 + +- [ ] contextIsolation 已启用 +- [ ] nodeIntegration 已禁用 +- [ ] preload 脚本无安全漏洞 +- [ ] IPC 消息验证 + +--- + +## 文件变更清单 + +### 新增文件 + +| 文件路径 | 说明 | +|---------|------| +| `electron/main.js` | 主进程入口 | +| `electron/preload.js` | 预加载脚本 | +| `electron/ipc/clipboard.js` | 剪贴板 IPC 处理 | +| `electron/ipc/file.js` | 文件系统 IPC 处理 | +| `electron/utils/paths.js` | 路径工具 | +| `public/js/electron-adapter.js` | 渲染进程适配层 | +| `public/icon.ico` | 应用图标 | + +### 修改文件 + +| 文件路径 | 修改内容 | +|---------|---------| +| `package.json` | 添加 Electron 配置和脚本 | +| `public/index.html` | 引入适配层,修改剪贴板逻辑 | +| `public/js/file-panel.js` | 增强文件功能,添加 Electron 支持 | + +--- + +## 时间估算 + +| 阶段 | 预计时间 | +|------|---------| +| 阶段一:项目初始化 | 30 分钟 | +| 阶段二:主进程开发 | 1 小时 | +| 阶段三:渲染进程适配 | 1 小时 | +| 阶段四:构建配置 | 30 分钟 | +| 阶段五:测试与优化 | 1 小时 | +| **总计** | **4 小时** | + +--- + +## 风险与注意事项 + +### 安全风险 + +1. **IPC 消息验证** - 所有 IPC 消息需要验证来源和参数 +2. **路径遍历攻击** - 文件操作需要验证路径合法性 +3. **远程代码加载** - 如果加载远程 URL,需要确保服务器安全 + +### 兼容性考虑 + +1. **保留浏览器兼容** - 应用应同时支持浏览器和 Electron 环境 +2. **优雅降级** - 在浏览器环境中使用现有方案 + +### 性能考虑 + +1. **剪贴板监听** - 避免频繁轮询剪贴板 +2. **大文件传输** - 使用流式处理避免内存溢出 + +--- + +## 后续增强 + +1. **自动更新** - 集成 electron-updater +2. **离线模式** - 支持本地 HTML 加载 +3. **多窗口** - 支持多显示器场景 +4. **快捷键** - 全局快捷键支持 +5. **系统托盘** - 最小化到托盘 + +--- + +## 结论 + +通过 Electron 迁移,可以完全解决浏览器安全限制问题: + +| 功能 | 迁移前 | 迁移后 | +|------|--------|--------| +| 剪贴板读取 | ❌ 受限 | ✅ 完全可用 | +| 剪贴板写入 | ❌ 受限 | ✅ 完全可用 | +| 文件选择 | ⚠️ 受限 | ✅ 完全可用 | +| 目录浏览 | ❌ 不可用 | ✅ 完全可用 | +| 文件保存 | ⚠️ 受限 | ✅ 完全可用 | + +迁移后,用户体验将显著提升,同时保持与现有服务端的完全兼容。 diff --git a/remote/docs/开发/前端开发.md b/remote/docs/开发/前端开发.md new file mode 100644 index 0000000..1d5dc68 --- /dev/null +++ b/remote/docs/开发/前端开发.md @@ -0,0 +1,1108 @@ +# 前端开发指南 + +本文档介绍 xc-remote 前端架构和各模块实现细节。 + +## 目录 + +- [前端架构说明](#前端架构说明) +- [VideoPlayer 模块](#videoplayer-模块) +- [InputHandler 模块](#inputhandler-模块) +- [FilePanel 模块](#filepanel-模块) +- [JSMpeg 集成说明](#jsmpeg-集成说明) +- [剪贴板同步实现](#剪贴板同步实现) + +--- + +## 前端架构说明 + +### 整体结构 + +前端采用原生 JavaScript 模块化设计,无框架依赖,核心模块独立封装。 + +``` +public/ +├── index.html # 主入口页面 +├── css/ +│ └── main.css # 样式文件 +└── js/ + ├── app.js # 应用初始化入口 + ├── player.js # 视频播放器模块 + ├── input.js # 输入事件处理模块 + ├── file-panel.js # 文件传输面板模块 + ├── utils.js # 工具函数 + └── jsmpeg.min.js # JSMpeg 解码库 +``` + +### 模块关系图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ index.html │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ app.js (入口) │ │ +│ │ ┌─────────────┐ ┌──────────────────────────┐ │ │ +│ │ │ VideoPlayer │◄───│ InputHandler │ │ │ +│ │ │ (播放器) │ │ (输入处理) │ │ │ +│ │ └──────┬──────┘ └──────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────┐ ┌──────────────────────────┐ │ │ +│ │ │ JSMpeg │ │ FilePanel │ │ │ +│ │ │ (解码库) │ │ (文件传输) │ │ │ +│ │ └─────────────┘ └──────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ utils.js (工具函数) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 初始化流程 + +```javascript +(function() { + let videoPlayer = null; + let inputHandler = null; + + function init() { + videoPlayer = new VideoPlayer('video-canvas', WS_URL); + videoPlayer.init(); + + inputHandler = new InputHandler(videoPlayer.getCanvas(), { + wsUrl: WS_URL + }); + inputHandler.init(); + } + + function destroy() { + if (inputHandler) { + inputHandler.destroy(); + inputHandler = null; + } + if (videoPlayer) { + videoPlayer.destroy(); + videoPlayer = null; + } + } + + window.addEventListener('beforeunload', destroy); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); +``` + +--- + +## VideoPlayer 模块 + +### 概述 + +VideoPlayer 负责视频流的接收和渲染,通过 JSMpeg 库解码 MPEG1 视频流并在 Canvas 上渲染。 + +### 类定义 + +```javascript +class VideoPlayer { + constructor(canvasId, wsUrl, options = {}) { + this.canvas = document.getElementById(canvasId); + this.wsUrl = wsUrl; + this.options = { + autoplay: true, + audio: false, + videoBufferSize: 512 * 1024, + ...options + }; + this.player = null; + } + + init() { + this.player = new JSMpeg.Player(this.wsUrl, { + canvas: this.canvas, + ...this.options + }); + return this; + } + + getCanvas() { + return this.canvas; + } + + getPlayer() { + return this.player; + } + + destroy() { + if (this.player) { + this.player.destroy(); + this.player = null; + } + } +} +``` + +### JSMpeg 播放器集成 + +JSMpeg 是一个用 JavaScript 实现的 MPEG1 视频解码器,支持通过 WebSocket 接收实时视频流。 + +**配置参数说明:** + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `canvas` | HTMLCanvasElement | - | 渲染目标 Canvas 元素 | +| `autoplay` | boolean | true | 是否自动播放 | +| `audio` | boolean | false | 是否启用音频解码 | +| `videoBufferSize` | number | 512KB | 视频缓冲区大小 | + +### Canvas 渲染流程 + +``` +┌──────────────┐ WebSocket ┌──────────────┐ +│ 服务端 │ ──────────────► │ JSMpeg │ +│ (MPEG1流) │ │ 解码器 │ +└──────────────┘ └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ Canvas │ + │ 渲染 │ + └──────────────┘ +``` + +### 播放器初始化和销毁 + +**初始化:** + +```javascript +const videoPlayer = new VideoPlayer('video-canvas', WS_URL); +videoPlayer.init(); +``` + +**销毁:** + +```javascript +videoPlayer.destroy(); +``` + +销毁时需要调用 JSMpeg 的 `destroy()` 方法释放资源,避免内存泄漏。 + +--- + +## InputHandler 模块 + +### 概述 + +InputHandler 负责捕获用户输入事件(鼠标、键盘),进行坐标转换和事件节流,然后通过 WebSocket 发送到服务端。 + +### 类定义结构 + +```javascript +class InputHandler { + constructor(canvas, options = {}) { + this.canvas = canvas; + this.wsUrl = options.wsUrl; + this.screenWidth = options.screenWidth || 1280; + this.screenHeight = options.screenHeight || 720; + + this.ws = null; + this.wsReady = false; + + this.lastMoveTime = 0; + this.MOVE_THROTTLE_MS = 33; + this.pendingMove = null; + this.moveThrottleTimer = null; + + this.pressedKeys = new Set(); + + this.codeToKey = { /* 键码映射表 */ }; + this._boundHandlers = { /* 事件处理器绑定 */ }; + } +} +``` + +### 鼠标事件处理 + +**事件绑定:** + +```javascript +_bindEvents() { + this.canvas.addEventListener('mousemove', this._boundHandlers.mousemove); + this.canvas.addEventListener('mousedown', this._boundHandlers.mousedown); + this.canvas.addEventListener('mouseup', this._boundHandlers.mouseup); + this.canvas.addEventListener('contextmenu', this._boundHandlers.contextmenu); + this.canvas.addEventListener('wheel', this._boundHandlers.wheel); +} +``` + +**鼠标移动处理:** + +```javascript +_handleMouseMove(e) { + const { x, y } = getScreenCoordinates( + e.clientX, + e.clientY, + this.canvas, + this.screenWidth, + this.screenHeight + ); + this._sendMouseMove(x, y); +} +``` + +**鼠标按键处理:** + +```javascript +_handleMouseDown(e) { + const button = e.button === 2 ? 'right' : 'left'; + this.sendInputEvent({ + type: 'mouseDown', + button: button + }); +} + +_handleMouseUp(e) { + const button = e.button === 2 ? 'right' : 'left'; + this.sendInputEvent({ + type: 'mouseUp', + button: button + }); +} +``` + +**滚轮事件处理:** + +```javascript +_handleWheel(e) { + e.preventDefault(); + const delta = -Math.sign(e.deltaY) * 120; + this.sendInputEvent({ + type: 'mouseWheel', + delta: delta + }); +} +``` + +### 键盘事件处理 + +**键码映射表:** + +```javascript +this.codeToKey = { + 'Enter': 'enter', 'Backspace': 'backspace', 'Tab': 'tab', + 'Escape': 'escape', 'Delete': 'delete', 'Home': 'home', + 'End': 'end', 'PageUp': 'pageup', 'PageDown': 'pagedown', + 'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right', + 'F1': 'f1', 'F2': 'f2', /* ... */, + 'ControlLeft': 'ctrl', 'ControlRight': 'ctrl', + 'AltLeft': 'alt', 'AltRight': 'alt', + 'ShiftLeft': 'shift', 'ShiftRight': 'shift', + 'MetaLeft': 'win', 'MetaRight': 'win', + 'Space': 'space', + 'Digit0': '0', /* ... */, + 'KeyA': 'a', 'KeyB': 'b', /* ... */ +}; +``` + +**键盘按下处理:** + +```javascript +_handleKeyDown(e) { + const key = this._getKeyFromEvent(e); + const keyId = key.toLowerCase(); + + if (!this.pressedKeys.has(keyId)) { + this.pressedKeys.add(keyId); + this.sendInputEvent({ + type: 'keyDown', + key: key + }); + } + + if (e.key === 'Tab' || e.key === ' ' || e.key.startsWith('Arrow')) { + e.preventDefault(); + } +} +``` + +**键盘释放处理:** + +```javascript +_handleKeyUp(e) { + const key = this._getKeyFromEvent(e); + const keyId = key.toLowerCase(); + + this.pressedKeys.delete(keyId); + this.sendInputEvent({ + type: 'keyUp', + key: key + }); +} +``` + +**窗口失焦处理:** + +```javascript +_handleBlur() { + this.pressedKeys.forEach(keyId => { + this.sendInputEvent({ + type: 'keyUp', + key: keyId + }); + }); + this.pressedKeys.clear(); +} +``` + +### 坐标转换逻辑 + +坐标转换将浏览器 Canvas 坐标转换为远程屏幕坐标: + +```javascript +function getScreenCoordinates(clientX, clientY, canvas, screenWidth, screenHeight) { + const rect = canvas.getBoundingClientRect(); + const canvasX = clientX - rect.left; + const canvasY = clientY - rect.top; + + const scaleX = screenWidth / rect.width; + const scaleY = screenHeight / rect.height; + + const screenX = Math.floor(canvasX * scaleX); + const screenY = Math.floor(canvasY * scaleY); + + return { x: screenX, y: screenY }; +} +``` + +**转换流程图:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ Canvas 坐标系 │ +│ ┌─────────────────────────────────────┐ │ +│ │ │ │ │ +│ │ (canvasX, canvasY) │ │ │ +│ │ • │ │ │ +│ │ │ │ │ +│ └─────────────────────────────────────┘ │ │ +│ Canvas 尺寸: rect.width x rect.height │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ 缩放转换 +┌─────────────────────────────────────────────────────────┐ +│ 远程屏幕坐标系 │ +│ ┌─────────────────────────────────────┐ │ +│ │ │ │ │ +│ │ (screenX, screenY) │ │ │ +│ │ • │ │ │ +│ │ │ │ │ +│ └─────────────────────────────────────┘ │ │ +│ 屏幕尺寸: screenWidth x screenHeight │ +└─────────────────────────────────────────────────────────┘ + +转换公式: + scaleX = screenWidth / canvasWidth + scaleY = screenHeight / canvasHeight + screenX = floor(canvasX * scaleX) + screenY = floor(canvasY * scaleY) +``` + +### 事件节流 + +鼠标移动事件频率很高,需要节流处理避免过多的 WebSocket 消息: + +```javascript +_sendMouseMove(x, y) { + this.pendingMove = { x, y }; + + const now = Date.now(); + if (now - this.lastMoveTime >= this.MOVE_THROTTLE_MS) { + this._flushMouseMove(); + } else if (!this.moveThrottleTimer) { + this.moveThrottleTimer = setTimeout(() => { + this._flushMouseMove(); + this.moveThrottleTimer = null; + }, this.MOVE_THROTTLE_MS - (now - this.lastMoveTime)); + } +} + +_flushMouseMove() { + if (this.pendingMove) { + this.sendInputEvent({ + type: 'mouseMove', + x: this.pendingMove.x, + y: this.pendingMove.y + }); + this.lastMoveTime = Date.now(); + this.pendingMove = null; + } +} +``` + +**节流策略:** + +- 节流间隔:33ms(约 30fps) +- 使用 `pendingMove` 缓存最新位置 +- 使用定时器确保最后一次移动被发送 + +--- + +## FilePanel 模块 + +### 概述 + +FilePanel 提供文件传输功能,支持文件浏览、分块上传、下载和传输进度显示。 + +### 类定义结构 + +```javascript +class FilePanel { + constructor() { + this.isVisible = false; + this.localPath = ''; + this.remotePath = ''; + this.localFiles = []; + this.remoteFiles = []; + this.selectedLocal = null; + this.selectedRemote = null; + this.transfers = []; + this.chunkSize = 5 * 1024 * 1024; // 5MB 分块 + } +} +``` + +### 文件浏览 + +**远程文件浏览:** + +```javascript +async refreshRemote() { + try { + const res = await fetch(`/api/files/browse?path=${encodeURIComponent(this.remotePath)}`); + const data = await res.json(); + + if (data.error) { + console.error('Browse error:', data.error); + return; + } + + this.remoteFiles = data.items || []; + this.remotePath = data.currentPath || ''; + + document.getElementById('remote-path').value = this.remotePath || '/'; + this.renderRemoteFiles(); + } catch (error) { + console.error('Failed to refresh remote:', error); + } +} +``` + +**文件列表渲染:** + +```javascript +renderRemoteFiles() { + const container = document.getElementById('remote-files'); + + if (this.remotePath) { + container.innerHTML = `
+ 📁 + .. +
`; + } else { + container.innerHTML = ''; + } + + this.remoteFiles.forEach(item => { + const div = document.createElement('div'); + div.className = `file-item ${item.isDirectory ? 'directory' : 'file'}`; + div.dataset.name = item.name; + div.dataset.isDirectory = item.isDirectory; + div.innerHTML = ` + ${item.isDirectory ? '📁' : this.getFileIcon(item.name)} + ${item.name} + ${item.isDirectory ? '' : this.formatSize(item.size)} + `; + container.appendChild(div); + }); +} +``` + +### 分块上传实现 + +分块上传支持大文件传输,将文件分割为多个块依次上传: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 分块上传流程 │ +└─────────────────────────────────────────────────────────────┘ + +┌──────────┐ POST /upload/start ┌──────────┐ +│ 客户端 │ ─────────────────────────► │ 服务端 │ +│ │ {filename, totalChunks, │ │ +│ │ fileSize} │ │ +└──────────┘ └──────────┘ + │ │ + │ ◄───── {fileId} ───── │ + │ │ + ▼ │ +┌──────────┐ POST /upload/chunk │ +│ 循环上传 │ ─────────────────────────► │ +│ 每个分块 │ {fileId, chunkIndex, chunk} │ +└──────────┘ ┌──────────┐ + │ │ 服务端 │ + │ (重复 totalChunks 次) │ 存储分块 │ + ▼ └──────────┘ +┌──────────┐ POST /upload/merge │ +│ 合并请求 │ ─────────────────────────► │ +│ │ {fileId, totalChunks, filename} │ +└──────────┘ ┌──────────┐ + │ 服务端 │ + │ 合并文件 │ + └──────────┘ +``` + +**上传实现代码:** + +```javascript +async uploadFile(item) { + const transferId = Date.now(); + this.addTransfer(transferId, item.name, 'upload', item.size); + + try { + const totalChunks = Math.ceil(item.size / this.chunkSize); + + // 1. 开始上传,获取 fileId + const startRes = await fetch('/api/files/upload/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filename: item.name, + totalChunks, + fileSize: item.size + }) + }); + + const { fileId } = await startRes.json(); + + // 2. 分块上传 + for (let i = 0; i < totalChunks; i++) { + const start = i * this.chunkSize; + const end = Math.min(start + this.chunkSize, item.size); + const chunk = item.file.slice(start, end); + + const formData = new FormData(); + formData.append('fileId', fileId); + formData.append('chunkIndex', i); + formData.append('chunk', chunk); + + await fetch('/api/files/upload/chunk', { + method: 'POST', + body: formData + }); + + const progress = Math.round(((i + 1) / totalChunks) * 100); + this.updateTransfer(transferId, progress); + } + + // 3. 合并文件 + const remoteFilePath = this.remotePath + ? `${this.remotePath}/${item.name}` + : item.name; + + await fetch('/api/files/upload/merge', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + fileId, + totalChunks, + filename: remoteFilePath + }) + }); + + this.completeTransfer(transferId); + } catch (error) { + console.error('Upload failed:', error); + this.failTransfer(transferId, error.message); + } +} +``` + +### 下载实现 + +下载使用流式读取,支持进度显示: + +```javascript +async downloadSelected() { + if (!this.selectedRemote || this.selectedRemote.isDirectory) return; + + const filename = this.selectedRemote.name; + const remoteFilePath = this.remotePath + ? `${this.remotePath}/${filename}` + : filename; + + const transferId = Date.now(); + this.addTransfer(transferId, filename, 'download', 0); + + try { + const res = await fetch(`/api/files/${encodeURIComponent(remoteFilePath)}`); + + if (!res.ok) { + throw new Error('Download failed'); + } + + const contentLength = parseInt(res.headers.get('Content-Length') || '0'); + this.updateTransferSize(transferId, contentLength); + + // 流式读取 + const reader = res.body.getReader(); + const chunks = []; + let received = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + received += value.length; + + const progress = contentLength > 0 + ? Math.round((received / contentLength) * 100) + : 0; + this.updateTransfer(transferId, progress); + } + + // 创建下载链接 + const blob = new Blob(chunks); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + + this.completeTransfer(transferId); + } catch (error) { + console.error('Download failed:', error); + this.failTransfer(transferId, error.message); + } +} +``` + +### 传输进度显示 + +**传输状态管理:** + +```javascript +addTransfer(id, name, type, size) { + const transfer = { id, name, type, size, progress: 0, status: 'transferring' }; + this.transfers.push(transfer); + this.renderTransfers(); +} + +updateTransfer(id, progress) { + const transfer = this.transfers.find(t => t.id === id); + if (transfer) { + transfer.progress = progress; + this.renderTransfers(); + } +} + +completeTransfer(id) { + const transfer = this.transfers.find(t => t.id === id); + if (transfer) { + transfer.status = 'completed'; + transfer.progress = 100; + this.renderTransfers(); + + // 3秒后移除已完成项 + setTimeout(() => { + this.transfers = this.transfers.filter(t => t.id !== id); + this.renderTransfers(); + }, 3000); + } +} +``` + +**进度渲染:** + +```javascript +renderTransfers() { + const container = document.getElementById('transfer-list'); + + if (this.transfers.length === 0) { + container.innerHTML = ''; + return; + } + + container.innerHTML = this.transfers.map(t => ` +
+ ${t.type === 'upload' ? '↑' : '↓'} + ${t.name} +
+
+
+ ${t.progress}% +
+ `).join(''); +} +``` + +--- + +## JSMpeg 集成说明 + +### JSMpeg 简介 + +JSMpeg 是一个纯 JavaScript 实现的 MPEG1 视频解码器,主要特点: + +- 纯 JavaScript 实现,无需插件 +- 支持 WebSocket 实时流 +- 低延迟,适合远程桌面场景 +- 支持 MPEG1 视频和 MP2 音频 + +### 集成方式 + +**引入库文件:** + +```html + +``` + +**创建播放器:** + +```javascript +const player = new JSMpeg.Player(wsUrl, { + canvas: document.getElementById('video-canvas'), + autoplay: true, + audio: false, + videoBufferSize: 512 * 1024 +}); +``` + +### MPEG1 视频解码流程 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ JSMpeg 解码流程 │ +└──────────────────────────────────────────────────────────────┘ + +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ WebSocket │────►│ 解复用器 │────►│ 视频解码器 │ +│ 接收数据 │ │ (Demuxer) │ │ (Decoder) │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ┌────────────────────┘ + │ + ▼ + ┌─────────────┐ ┌─────────────┐ + │ 渲染器 │────►│ Canvas │ + │ (Renderer) │ │ 显示 │ + └─────────────┘ └─────────────┘ +``` + +### 服务端视频编码要求 + +服务端需要将屏幕捕获编码为 MPEG1 格式: + +- 视频编码:MPEG1 +- 帧率:建议 30fps +- 码率:根据网络情况调整 +- 关键帧间隔:建议 1-2 秒 + +--- + +## 剪贴板同步实现 + +### 概述 + +剪贴板同步允许在本地和远程之间同步文本内容,通过 Ctrl+C/V 快捷键触发。 + +### 实现架构 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 剪贴板同步流程 │ +└──────────────────────────────────────────────────────────────┘ + +Ctrl+C (本地) Ctrl+V (本地) + │ │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ syncFromRemote │ syncToRemote │ +│ 从远程获取剪贴板 │ 发送到远程 │ +└──────┬──────┘ └──────┬──────┘ + │ │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ clipboardGet│ │ clipboardSet│ +│ WebSocket消息│ │ WebSocket消息│ +└──────┬──────┘ └──────┬──────┘ + │ │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ 服务端 │ │ 服务端 │ +│ 读取剪贴板 │ │ 写入剪贴板 │ +└──────┬──────┘ └──────┬──────┘ + │ │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│clipboardData│ │ 远程系统 │ +│ 返回数据 │ │ 剪贴板更新 │ +└──────┬──────┘ └─────────────┘ + │ + ▼ +┌─────────────┐ +│ 本地剪贴板 │ +│ navigator. │ +│ clipboard. │ +│ writeText() │ +└─────────────┘ +``` + +### WebSocket 连接管理 + +```javascript +let clipboardWs = null; +let isClipboardSyncing = false; + +function connectClipboardWs() { + if (clipboardWs && clipboardWs.readyState === WebSocket.OPEN) { + return clipboardWs; + } + + clipboardWs = new WebSocket(WS_URL); + + clipboardWs.onmessage = async (e) => { + if (e.data instanceof Blob) return; + + try { + const msg = JSON.parse(e.data); + + if (msg.type === 'clipboardData') { + if (msg.contentType === 'text' && msg.data) { + try { + await navigator.clipboard.writeText(msg.data); + console.log('Clipboard synced from remote'); + } catch (err) { + console.error('Failed to write local clipboard:', err); + } + } + } else if (msg.type === 'clipboardTooLarge') { + console.warn('Remote clipboard content too large:', msg.size); + } + } catch (err) {} + }; + + clipboardWs.onopen = () => console.log('Clipboard sync ready'); + clipboardWs.onclose = () => { clipboardWs = null; }; + clipboardWs.onerror = () => { clipboardWs = null; }; + + return clipboardWs; +} +``` + +### Ctrl+C 同步(从远程获取) + +```javascript +async function syncFromRemote() { + if (isClipboardSyncing) return; + isClipboardSyncing = true; + + try { + const ws = connectClipboardWs(); + const sendRequest = () => { + ws.send(JSON.stringify({ type: 'clipboardGet' })); + }; + + if (ws.readyState === WebSocket.OPEN) { + sendRequest(); + } else { + ws.onopen = sendRequest; + } + } finally { + setTimeout(() => { isClipboardSyncing = false; }, 500); + } +} +``` + +### Ctrl+V 同步(发送到远程) + +```javascript +async function syncToRemote() { + if (isClipboardSyncing) return; + isClipboardSyncing = true; + + try { + let text = ''; + try { + text = await navigator.clipboard.readText(); + } catch (err) { + console.warn('Cannot read local clipboard'); + return; + } + + if (!text) return; + + const ws = connectClipboardWs(); + const sendRequest = () => { + ws.send(JSON.stringify({ + type: 'clipboardSet', + contentType: 'text', + data: text + })); + console.log('Clipboard synced to remote'); + }; + + if (ws.readyState === WebSocket.OPEN) { + sendRequest(); + } else { + ws.onopen = sendRequest; + } + } finally { + setTimeout(() => { isClipboardSyncing = false; }, 500); + } +} +``` + +### 快捷键监听 + +```javascript +document.addEventListener('keydown', async (e) => { + // Ctrl+C: 从远程同步到本地 + if (e.ctrlKey && e.key === 'c' && !e.shiftKey && !e.altKey) { + if (document.activeElement.tagName === 'INPUT' || + document.activeElement.tagName === 'TEXTAREA') { + return; + } + e.preventDefault(); + await syncFromRemote(); + } + + // Ctrl+V: 从本地同步到远程 + if (e.ctrlKey && e.key === 'v' && !e.shiftKey && !e.altKey) { + if (document.activeElement.tagName === 'INPUT' || + document.activeElement.tagName === 'TEXTAREA') { + return; + } + e.preventDefault(); + await syncToRemote(); + } +}); +``` + +### 防抖处理 + +使用 `isClipboardSyncing` 标志防止重复触发: + +```javascript +let isClipboardSyncing = false; + +async function syncFromRemote() { + if (isClipboardSyncing) return; + isClipboardSyncing = true; + + try { + // 执行同步操作 + } finally { + setTimeout(() => { isClipboardSyncing = false; }, 500); + } +} +``` + +--- + +## 工具函数 + +### getCookie + +获取 Cookie 值: + +```javascript +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return ''; +} +``` + +### createReconnectingWebSocket + +创建自动重连的 WebSocket 连接: + +```javascript +function createReconnectingWebSocket(url, options = {}) { + const { + onOpen = () => {}, + onClose = () => {}, + onMessage = () => {}, + onError = () => {}, + maxDelay = 30000 + } = options; + + let ws = null; + let reconnectDelay = 1000; + let reconnectTimer = null; + let isManualClose = false; + + function connect() { + ws = new WebSocket(url); + + ws.onopen = () => { + reconnectDelay = 1000; + onOpen(ws); + }; + + ws.onmessage = (e) => { + onMessage(e, ws); + }; + + ws.onclose = () => { + onClose(); + if (!isManualClose) { + reconnectTimer = setTimeout(() => { + reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); + connect(); + }, reconnectDelay); + } + }; + + ws.onerror = (err) => { + onError(err); + }; + } + + function close() { + isManualClose = true; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + if (ws) { + ws.close(); + } + } + + function getWebSocket() { + return ws; + } + + function isReady() { + return ws && ws.readyState === WebSocket.OPEN; + } + + connect(); + + return { + getWebSocket, + isReady, + close + }; +} +``` + +**重连策略:** + +- 初始延迟:1秒 +- 指数退避:每次重连延迟翻倍 +- 最大延迟:30秒 +- 手动关闭时不重连 diff --git a/remote/docs/开发/安全指南.md b/remote/docs/开发/安全指南.md new file mode 100644 index 0000000..83cf653 --- /dev/null +++ b/remote/docs/开发/安全指南.md @@ -0,0 +1,627 @@ +# 安全指南 + +本文档详细说明了 Remote Control 项目的安全机制和最佳实践。 + +## 目录 + +- [1. 密码安全](#1-密码安全) +- [2. JWT Token 管理](#2-jwt-token-管理) +- [3. 认证中间件](#3-认证中间件) +- [4. 安全建议](#4-安全建议) +- [5. 常见安全风险](#5-常见安全风险) + +--- + +## 1. 密码安全 + +### 1.1 bcrypt 哈希算法 + +本项目使用 bcrypt 算法对密码进行哈希处理。bcrypt 是一种专门为密码存储设计的哈希算法,具有以下优势: + +- **自带盐值**:每次哈希自动生成随机盐值,防止彩虹表攻击 +- **计算成本可调**:通过 cost factor 控制计算复杂度,抵御暴力破解 +- **抗 GPU/ASIC 攻击**:内存密集型设计,不适合硬件加速 + +```javascript +const bcrypt = require('bcrypt'); + +const BCRYPT_COST = 12; + +async function hashPassword(password) { + return bcrypt.hash(password, BCRYPT_COST); +} + +async function verifyPassword(password, hash) { + return bcrypt.compare(password, hash); +} +``` + +**Cost Factor 说明**: +- 项目使用 `BCRYPT_COST = 12`(约 4096 轮迭代) +- Cost 值每增加 1,计算时间翻倍 +- 推荐范围:10-12(生产环境),可根据服务器性能调整 + +### 1.2 密码迁移脚本使用 + +项目提供了密码迁移脚本 `scripts/migrate-password.js`,用于将明文密码转换为 bcrypt 哈希值。 + +**使用方法**: + +```bash +node scripts/migrate-password.js +``` + +**交互示例**: + +``` +=== 密码迁移脚本 === +此脚本将明文密码转换为 bcrypt 哈希值 + +请输入要哈希的密码 (或输入 q 退出): MySecurePassword123 + +--- 结果 --- +明文密码: MySecurePassword123 +bcrypt 哈希: $2b$10$abcdefghijklmnopqrstuvwxABCDEFGHIJ + +请将哈希值更新到环境变量或配置文件中 +``` + +**配置方式**: + +将生成的哈希值配置到环境变量: + +```bash +# .env 文件 +REMOTE_SECURITY_PASSWORD=$2b$10$abcdefghijklmnopqrstuvwxABCDEFGHIJ +``` + +### 1.3 明文密码 vs 哈希密码 + +系统支持两种密码配置方式,但**强烈推荐使用哈希密码**: + +| 特性 | 明文密码 | 哈希密码 | +|------|----------|----------| +| 配置方式 | `REMOTE_SECURITY_PASSWORD=mypassword` | `REMOTE_SECURITY_PASSWORD=$2b$12$...` | +| 识别方式 | 不以 `$2b$` 开头 | 以 `$2b$` 开头 | +| 安全性 | 低 | 高 | +| 泄露风险 | 配置文件泄露即密码泄露 | 哈希值泄露无法反推原密码 | + +**密码验证流程**: + +```javascript +async authenticate(password) { + if (this.isHashed) { + return await this.verifyPassword(password, this.passwordHash); + } else { + return password === this.passwordHash; + } +} +``` + +> ⚠️ **警告**:明文密码仅用于开发测试环境,生产环境必须使用 bcrypt 哈希密码。 + +--- + +## 2. JWT Token 管理 + +### 2.1 Token 生成 + +Token 使用 `jsonwebtoken` 库生成,包含用户标识和签发时间: + +```javascript +const TOKEN_EXPIRY = '24h'; + +generateToken(payload) { + const tokenPayload = { + userId: payload.userId || 'default-user', + iat: Math.floor(Date.now() / 1000) + }; + + const options = { + expiresIn: TOKEN_EXPIRY + }; + + return jwt.sign(tokenPayload, this.secret, options); +} +``` + +**Token 结构**: +- Header:算法类型 (HS256) +- Payload:`userId`、`iat`(签发时间)、`exp`(过期时间) +- Signature:使用密钥签名 + +### 2.2 Token 验证 + +```javascript +verifyToken(token) { + if (!token) { + return null; + } + + try { + const decoded = jwt.verify(token, this.secret); + return decoded; + } catch (error) { + if (error.name === 'TokenExpiredError') { + console.warn('Token expired', { expiredAt: error.expiredAt }); + } else if (error.name === 'JsonWebTokenError') { + console.warn('Invalid token', { error: error.message }); + } + return null; + } +} +``` + +**验证结果**: +- 成功:返回解码后的 payload 对象 +- Token 过期:返回 `null`,记录过期时间 +- Token 无效:返回 `null`,记录错误信息 + +### 2.3 Token 有效期 + +- **默认有效期**:24 小时 +- **续期策略**:Token 过期后需重新认证 +- **建议**:根据业务需求调整有效期 + +```javascript +const TOKEN_EXPIRY = '24h'; + +const options = { + expiresIn: TOKEN_EXPIRY +}; +``` + +### 2.4 密钥配置 + +密钥优先级顺序: + +```javascript +_getSecret() { + if (process.env.JWT_SECRET) { + return process.env.JWT_SECRET; + } + + if (process.env.REMOTE_SECURITY_PASSWORD) { + return process.env.REMOTE_SECURITY_PASSWORD; + } + + return 'remote-control-default-secret-change-in-production'; +} +``` + +**配置优先级**: +1. `JWT_SECRET` - 推荐使用 +2. `REMOTE_SECURITY_PASSWORD` - 备用选项 +3. 默认密钥 - 仅开发环境,生产环境必须配置 + +**推荐配置**: + +```bash +# .env 文件 +JWT_SECRET=your-very-long-random-secret-key-at-least-32-characters +REMOTE_SECURITY_PASSWORD=$2b$12$your-bcrypt-hashed-password +``` + +> ⚠️ **警告**:生产环境必须设置 `JWT_SECRET`,使用默认密钥将导致严重安全风险。 + +--- + +## 3. 认证中间件 + +### 3.1 Token 提取方式 + +认证中间件支持多种 Token 提取方式: + +```javascript +function extractToken(req) { + if (req.headers.authorization) { + const parts = req.headers.authorization.split(' '); + if (parts.length === 2 && parts[0] === 'Bearer') { + return parts[1]; + } + } + + if (req.cookies) { + if (req.cookies.token) { + return req.cookies.token; + } + if (req.cookies.auth) { + return req.cookies.auth; + } + } + + if (req.query && req.query.token) { + return req.query.token; + } + + return null; +} +``` + +**提取顺序**: +1. **Authorization Header**:`Authorization: Bearer ` +2. **Cookie**:`token` 或 `auth` 字段 +3. **Query Parameter**:`?token=` + +### 3.2 密码验证 + +当 Token 无效或不存在时,支持通过密码进行认证: + +```javascript +const password = req.query.password || req.body?.password; + +if (password) { + const isValid = await authService.authenticate(password); + if (isValid) { + req.user = { userId: 'default-user' }; + res.locals.authenticated = true; + return next(); + } +} +``` + +**密码传递方式**: +- Query Parameter:`?password=` +- Request Body:`{ "password": "" }` + +### 3.3 认证流程 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 认证中间件流程 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. 检查是否配置密码 │ +│ └─ 无密码配置 → 跳过认证,允许访问 │ +│ │ +│ 2. 提取 Token │ +│ ├─ Authorization Header │ +│ ├─ Cookie │ +│ └─ Query Parameter │ +│ │ +│ 3. 验证 Token │ +│ ├─ 有效 → 设置用户信息,允许访问 │ +│ └─ 无效/过期 → 继续下一步 │ +│ │ +│ 4. 提取密码 │ +│ ├─ Query Parameter │ +│ └─ Request Body │ +│ │ +│ 5. 验证密码 │ +│ ├─ 正确 → 允许访问 │ +│ └─ 错误 → 返回 401 │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**完整代码**: + +```javascript +async function authMiddleware(req, res, next) { + const authService = AuthService.getInstance(); + const tokenManager = TokenManager.getInstance(); + + if (!authService.hasPassword()) { + req.user = { userId: 'default-user' }; + res.locals.authenticated = true; + return next(); + } + + const token = extractToken(req); + + if (token) { + const decoded = tokenManager.verifyToken(token); + if (decoded) { + req.user = { userId: decoded.userId }; + res.locals.authenticated = true; + return next(); + } + } + + const password = req.query.password || req.body?.password; + + if (password) { + const isValid = await authService.authenticate(password); + if (isValid) { + req.user = { userId: 'default-user' }; + res.locals.authenticated = true; + return next(); + } + } + + res.status(401).json({ + error: 'Authentication required', + code: 'AUTH_REQUIRED' + }); +} +``` + +--- + +## 4. 安全建议 + +### 4.1 使用强密码 + +- **长度**:至少 12 个字符 +- **复杂度**:包含大小写字母、数字、特殊字符 +- **避免**:常见单词、生日、连续字符 + +```bash +# 生成强密码示例 +openssl rand -base64 16 +``` + +### 4.2 启用 HTTPS + +**为什么需要 HTTPS**: +- 加密传输数据,防止中间人攻击 +- 保护 Token 和密码不被窃听 +- 防止内容被篡改 + +**配置示例(Nginx)**: + +```nginx +server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; +} + +server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; +} +``` + +### 4.3 定期更换密钥 + +**建议更换周期**: +- JWT_SECRET:每 3-6 个月 +- 密码:每 3 个月 + +**更换步骤**: + +1. 生成新密钥: +```bash +openssl rand -base64 32 +``` + +2. 更新环境变量 +3. 重启服务(会使所有现有 Token 失效) +4. 通知用户重新登录 + +### 4.4 限制访问 IP + +**Nginx 配置示例**: + +```nginx +location / { + allow 192.168.1.0/24; + allow 10.0.0.0/8; + deny all; + + proxy_pass http://localhost:3000; +} +``` + +**应用层限制**: + +```javascript +const ALLOWED_IPS = ['192.168.1.0/24', '10.0.0.0/8']; + +function ipFilter(req, res, next) { + const clientIp = req.ip || req.connection.remoteAddress; + + if (!isIpAllowed(clientIp, ALLOWED_IPS)) { + return res.status(403).json({ error: 'Access denied' }); + } + + next(); +} +``` + +### 4.5 使用 FRP Token + +如果通过 FRP 进行内网穿透,建议配置 FRP Token: + +**frps.ini(服务端)**: + +```ini +[common] +bind_port = 7000 +authentication_method = token +token = your-secure-frp-token +``` + +**frpc.ini(客户端)**: + +```ini +[common] +server_addr = your-server.com +server_port = 7000 +authentication_method = token +token = your-secure-frp-token + +[web] +type = http +local_port = 3000 +custom_domains = your-domain.com +``` + +--- + +## 5. 常见安全风险 + +### 5.1 密码泄露 + +**风险来源**: +- 配置文件被提交到版本控制 +- 日志中记录敏感信息 +- 服务器被入侵 + +**防护措施**: + +1. **使用环境变量**: +```bash +# .env 文件(添加到 .gitignore) +REMOTE_SECURITY_PASSWORD=$2b$12$... +JWT_SECRET=your-secret-key +``` + +2. **检查日志输出**: +```javascript +// 错误示例 +logger.info('User login', { password: password }); + +// 正确示例 +logger.info('User login', { userId: userId }); +``` + +3. **文件权限控制**: +```bash +chmod 600 .env +chown app:app .env +``` + +### 5.2 中间人攻击 + +**攻击场景**: +- HTTP 明文传输被监听 +- 公共 WiFi 环境被劫持 +- DNS 污染 + +**防护措施**: + +1. **强制 HTTPS**: +```javascript +app.use((req, res, next) => { + if (!req.secure && req.get('x-forwarded-proto') !== 'https') { + return res.redirect(`https://${req.get('host')}${req.url}`); + } + next(); +}); +``` + +2. **设置安全响应头**: +```javascript +app.use((req, res, next) => { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + next(); +}); +``` + +3. **使用 VPN**:在公共网络环境访问敏感服务 + +### 5.3 XSS 攻击 + +**攻击场景**: +- 用户输入未转义直接渲染 +- 恶意脚本窃取 Token +- Cookie 被劫持 + +**防护措施**: + +1. **设置 Cookie 安全属性**: +```javascript +res.cookie('token', token, { + httpOnly: true, + secure: true, + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000 +}); +``` + +2. **内容安全策略(CSP)**: +```javascript +res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self'" +); +``` + +3. **输入验证和转义**: +```javascript +const escapeHtml = (str) => { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; +``` + +### 5.4 CSRF 攻击 + +**攻击场景**: +- 恶意网站发送伪造请求 +- 利用用户已认证状态 +- 执行未授权操作 + +**防护措施**: + +1. **SameSite Cookie**: +```javascript +res.cookie('token', token, { + sameSite: 'strict' +}); +``` + +2. **CSRF Token**: +```javascript +const csrf = require('csurf'); +const csrfProtection = csrf({ cookie: true }); + +app.get('/form', csrfProtection, (req, res) => { + res.json({ csrfToken: req.csrfToken() }); +}); + +app.post('/submit', csrfProtection, (req, res) => { + // 处理请求 +}); +``` + +3. **验证 Referer 头**: +```javascript +app.use((req, res, next) => { + const referer = req.get('referer'); + const allowedOrigins = ['https://your-domain.com']; + + if (referer && !allowedOrigins.some(origin => referer.startsWith(origin))) { + return res.status(403).json({ error: 'Invalid referer' }); + } + + next(); +}); +``` + +--- + +## 安全检查清单 + +在部署到生产环境前,请确保完成以下检查: + +- [ ] 使用 bcrypt 哈希密码(非明文) +- [ ] 配置强随机 JWT_SECRET +- [ ] 启用 HTTPS +- [ ] 设置 Cookie 安全属性(httpOnly, secure, sameSite) +- [ ] 配置安全响应头(HSTS, CSP) +- [ ] 敏感文件未提交到版本控制 +- [ ] 日志不包含敏感信息 +- [ ] 配置 IP 访问限制(如适用) +- [ ] FRP 配置 Token 认证(如适用) +- [ ] 定期更换密钥和密码 + +--- + +## 相关文件 + +- [AuthService.js](../../src/services/auth/AuthService.js) - 认证服务实现 +- [TokenManager.js](../../src/services/auth/TokenManager.js) - Token 管理器 +- [auth.js](../../src/middlewares/auth.js) - 认证中间件 +- [migrate-password.js](../../scripts/migrate-password.js) - 密码迁移脚本 diff --git a/remote/docs/开发/服务开发.md b/remote/docs/开发/服务开发.md new file mode 100644 index 0000000..732e1cb --- /dev/null +++ b/remote/docs/开发/服务开发.md @@ -0,0 +1,1261 @@ +# 服务开发指南 + +本文档详细介绍远程控制系统中各核心服务的实现原理和开发指南。 + +## 目录 + +1. [FFmpegEncoder 编码器服务](#1-ffmpegencoder-编码器服务) +2. [PowerShellInput 输入服务](#2-powershellinput-输入服务) +3. [AuthService 认证服务](#3-authservice-认证服务) +4. [TokenManager 服务](#4-tokenmanager-服务) +5. [ClipboardService 剪贴板服务](#5-clipboardservice-剪贴板服务) +6. [FileService 文件服务](#6-fileservice-文件服务) +7. [FRPService 内网穿透服务](#7-frpservice-内网穿透服务) +8. [GiteaService Git 服务](#8-giteaservice-git-服务) + +--- + +## 1. FFmpegEncoder 编码器服务 + +FFmpegEncoder 是视频流编码服务的核心实现,负责将屏幕画面实时编码为 MPEG1 视频流。 + +### 1.1 FFmpeg 命令参数 + +编码器通过构造 FFmpeg 命令行参数来实现屏幕捕获和编码: + +```javascript +_getArgs() { + const targetWidth = this.resolution.width; + const targetHeight = this.resolution.height; + + const args = [ + '-hide_banner', // 隐藏版本信息 + '-loglevel', 'error', // 仅输出错误日志 + '-f', 'gdigrab', // 使用 GDI 捕获输入 + '-framerate', String(this.fps), // 输入帧率 + '-i', 'desktop', // 输入源为桌面 + '-vf', `scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease`, // 视频缩放 + '-c:v', 'mpeg1video', // 视频编码器 + '-b:v', this.bitrate, // 视频比特率 + '-bf', '0', // 禁用 B 帧 + '-g', String(this.gop), // GOP 大小 + '-r', String(this.fps), // 输出帧率 + '-f', 'mpegts', // 输出格式为 MPEG-TS + '-flush_packets', '1', // 立即刷新数据包 + 'pipe:1' // 输出到标准输出 + ]; + + return args; +} +``` + +**参数说明:** + +| 参数 | 说明 | +|------|------| +| `-f gdigrab` | Windows GDI 屏幕捕获方式 | +| `-i desktop` | 捕获整个桌面 | +| `-c:v mpeg1video` | 使用 MPEG1 视频编码,兼容性好 | +| `-bf 0` | 禁用 B 帧以降低延迟 | +| `-f mpegts` | MPEG-TS 容器格式,支持流式传输 | +| `pipe:1` | 输出到标准输出,便于 Node.js 读取 | + +### 1.2 屏幕捕获流程 + +屏幕捕获流程分为三个阶段:初始化、启动、数据传输。 + +```javascript +class FFmpegEncoder extends StreamService { + constructor() { + super(); + const streamConfig = config.getSection('stream') || {}; + this.fps = streamConfig.fps || 30; + this.bitrate = streamConfig.bitrate || '2M'; + this.gop = streamConfig.gop || 30; + this.resolution = streamConfig.resolution || { width: 1920, height: 1080 }; + this.ffmpegProcess = null; + this.running = false; + this._getScreenResolution(); + } +} +``` + +**获取屏幕分辨率:** + +```javascript +_getScreenResolution() { + try { + const output = execSync( + 'powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height"', + { encoding: 'utf8' } + ); + const lines = output.trim().split('\n'); + if (lines.length >= 2) { + this.screenWidth = parseInt(lines[0].trim(), 10); + this.screenHeight = parseInt(lines[1].trim(), 10); + } + } catch (error) { + logger.warn('Failed to get screen resolution, using defaults'); + } +} +``` + +**启动编码进程:** + +```javascript +_startProcess() { + const args = this._getArgs(); + this.ffmpegProcess = spawn(ffmpegPath, args); + this.emit('start'); + + this.ffmpegProcess.stdout.on('data', (chunk) => { + this.emit('data', chunk); // 将编码数据发送给客户端 + }); + + this.ffmpegProcess.on('close', (code) => { + if (code !== 0 && code !== null && this.running) { + this._handleCrash(); + } + }); +} +``` + +### 1.3 错误重试机制 + +FFmpegEncoder 实现了自动重试机制,确保编码服务的稳定性: + +```javascript +constructor() { + this.retryCount = 0; + this.maxRetries = 3; + this.retryDelay = 5000; + this.retryTimer = null; +} + +_handleCrash() { + this.retryCount++; + + if (this.retryCount <= this.maxRetries) { + logger.warn(`FFmpeg crashed, retrying (${this.retryCount}/${this.maxRetries})`); + + this.retryTimer = setTimeout(() => { + if (this.running) { + this._startProcess(); + } + }, this.retryDelay); + } else { + logger.error('FFmpeg max retries exceeded, stopping'); + this.running = false; + this.emit('error', new Error('FFmpeg max retries exceeded')); + this.emit('stop'); + } +} +``` + +**重试机制流程图:** + +``` +FFmpeg 崩溃 + │ + ▼ +重试计数 +1 + │ + ▼ +┌─────────────────┐ +│ 重试次数 <= 3? │ +└────────┬────────┘ + │ + ┌────┴────┐ + │ │ + 是 否 + │ │ + ▼ ▼ +等待 5 秒 停止服务 + │ + ▼ +重新启动进程 +``` + +--- + +## 2. PowerShellInput 输入服务 + +PowerShellInput 通过嵌入 PowerShell 脚本调用 Windows API 实现远程输入控制。 + +### 2.1 PowerShell 脚本嵌入 + +服务将完整的 PowerShell 脚本嵌入到 Node.js 代码中: + +```javascript +const POWERSHELL_SCRIPT = ` +$env:PSModulePath += ';.' +Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public class Input { + [DllImport("user32.dll")] + public static extern bool SetCursorPos(int X, int Y); + [DllImport("user32.dll")] + public static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo); + [DllImport("user32.dll")] + public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, uint dwExtraInfo); + [DllImport("user32.dll")] + public static extern short GetAsyncKeyState(int vKey); +} +'@ -Language CSharp -ErrorAction SilentlyContinue + +while ($true) { + $line = [Console]::In.ReadLine() + if ($line -eq $null) { break } + if ($line -eq '') { continue } + + try { + $parts = $line -split ' ' + $cmd = $parts[0] + + switch ($cmd) { + 'move' { + $x = [int]$parts[1] + $y = [int]$parts[2] + [Input]::SetCursorPos($x, $y) + } + 'down' { + $btn = $parts[1] + $flag = if ($btn -eq 'right') { 8 } else { 2 } + [Input]::mouse_event($flag, 0, 0, 0, 0) + } + 'up' { + $btn = $parts[1] + $flag = if ($btn -eq 'right') { 16 } else { 4 } + [Input]::mouse_event($flag, 0, 0, 0, 0) + } + 'wheel' { + $delta = [int]$parts[1] + [Input]::mouse_event(2048, 0, 0, $delta, 0) + } + 'kdown' { + $vk = [int]$parts[1] + [Input]::keybd_event([byte]$vk, 0, 0, 0) + } + 'kup' { + $vk = [int]$parts[1] + [Input]::keybd_event([byte]$vk, 0, 2, 0) + } + } + } catch { + Write-Error $_.Exception.Message + } +} +`; +``` + +### 2.2 Windows API 调用 + +通过 P/Invoke 机制调用 user32.dll 中的输入相关 API: + +| API 函数 | 功能 | 参数说明 | +|----------|------|----------| +| `SetCursorPos` | 设置鼠标位置 | X, Y 坐标 | +| `mouse_event` | 模拟鼠标事件 | dwFlags: 事件类型, dx/dy: 移动量, dwData: 滚轮量 | +| `keybd_event` | 模拟键盘事件 | bVk: 虚拟键码, dwFlags: 事件类型 | + +**鼠标事件标志:** + +```javascript +// 鼠标按下 +'down' { + $btn = $parts[1] + $flag = if ($btn -eq 'right') { 8 } else { 2 } // 8=右键按下, 2=左键按下 + [Input]::mouse_event($flag, 0, 0, 0, 0) +} + +// 鼠标释放 +'up' { + $btn = $parts[1] + $flag = if ($btn -eq 'right') { 16 } else { 4 } // 16=右键释放, 4=左键释放 + [Input]::mouse_event($flag, 0, 0, 0, 0) +} + +// 鼠标滚轮 +'wheel' { + $delta = [int]$parts[1] + [Input]::mouse_event(2048, 0, 0, $delta, 0) // 2048=滚轮事件 +} +``` + +### 2.3 虚拟键码映射 + +服务维护了完整的虚拟键码映射表: + +```javascript +const VK_CODES = { + // 控制键 + 'enter': 13, 'backspace': 8, 'tab': 9, 'escape': 27, + 'delete': 46, 'home': 36, 'end': 35, 'pageup': 33, + 'pagedown': 34, + + // 方向键 + 'up': 38, 'down': 40, 'left': 37, 'right': 39, + + // 功能键 + 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, + 'f5': 116, 'f6': 117, 'f7': 118, 'f8': 119, + 'f9': 120, 'f10': 121, 'f11': 122, 'f12': 123, + + // 修饰键 + 'ctrl': 17, 'alt': 18, 'shift': 16, 'win': 91, + 'space': 32, + + // 符号键 + ',': 188, '.': 190, '/': 191, ';': 186, "'": 222, + '[': 219, ']': 221, '\\': 220, '-': 189, '=': 187, + '`': 192 +}; +``` + +**键码转换函数:** + +```javascript +_getVkCode(key) { + const lowerKey = key.toLowerCase(); + if (VK_CODES[lowerKey]) { + return VK_CODES[lowerKey]; + } + + if (key.length === 1) { + if (VK_CODES[key]) { + return VK_CODES[key]; + } + return key.toUpperCase().charCodeAt(0); // 字母键直接取 ASCII 码 + } + + return null; +} +``` + +**输入方法实现:** + +```javascript +async mouseMove(x, y) { + if (!this.mouseEnabled) return; + await this._sendCommand(`move ${Math.floor(x)} ${Math.floor(y)}`); +} + +async keyDown(key) { + if (!this.keyboardEnabled) return; + const vk = this._getVkCode(key); + if (vk) { + await this._sendCommand(`kdown ${vk}`); + } +} + +async keyPress(key) { + if (!this.keyboardEnabled) return; + await this.keyDown(key); + await new Promise(r => setTimeout(r, 10)); + await this.keyUp(key); +} +``` + +--- + +## 3. AuthService 认证服务 + +AuthService 实现基于 bcrypt 的密码哈希和验证功能。 + +### 3.1 bcrypt 密码哈希 + +服务使用 bcrypt 算法进行密码哈希,提供高安全性: + +```javascript +const bcrypt = require('bcrypt'); +const BCRYPT_COST = 12; // 计算成本因子 +const BCRYPT_HASH_PREFIX = '$2b$'; // bcrypt 哈希前缀 + +class AuthService { + async hashPassword(password) { + return bcrypt.hash(password, BCRYPT_COST); + } + + async verifyPassword(password, hash) { + return bcrypt.compare(password, hash); + } +} +``` + +**bcrypt 特点:** + +- 自适应计算成本,可随硬件性能提升调整 +- 内置盐值,防止彩虹表攻击 +- 抗 GPU/ASIC 破解 + +### 3.2 密码验证流程 + +AuthService 支持两种密码存储方式:明文密码和 bcrypt 哈希。 + +```javascript +_initializePassword() { + const password = process.env.REMOTE_SECURITY_PASSWORD; + + if (!password) { + logger.warn('No password configured. Authentication will be disabled.'); + return; + } + + if (password.startsWith(BCRYPT_HASH_PREFIX)) { + this.passwordHash = password; + this.isHashed = true; + logger.info('AuthService initialized with bcrypt hash password'); + } else { + this.passwordHash = password; + this.isHashed = false; + logger.info('AuthService initialized with plaintext password'); + } +} +``` + +**认证流程:** + +```javascript +async authenticate(password) { + if (!this.passwordHash) { + logger.debug('Authentication skipped: no password configured'); + return true; // 未配置密码时跳过认证 + } + + if (!password) { + logger.warn('Authentication failed: no password provided'); + return false; + } + + try { + if (this.isHashed) { + // 直接与存储的哈希比较 + const isValid = await this.verifyPassword(password, this.passwordHash); + return isValid; + } else { + // 明文密码比较 + if (password === this.passwordHash) { + return true; + } + return false; + } + } catch (error) { + logger.error('Authentication error', { error: error.message }); + return false; + } +} +``` + +**认证流程图:** + +``` +客户端请求 + │ + ▼ +检查密码配置 ──无──→ 跳过认证,返回成功 + │ + 有 + │ + ▼ +检查密码提供 ──无──→ 返回失败 + │ + 有 + │ + ▼ +┌─────────────────┐ +│ 密码是否已哈希?│ +└────────┬────────┘ + │ + ┌────┴────┐ + │ │ + 是 否 + │ │ + ▼ ▼ +bcrypt比较 明文比较 + │ │ + └────┬────┘ + │ + ▼ + 返回结果 +``` + +**单例模式实现:** + +```javascript +let instance = null; + +class AuthService { + constructor() { + if (instance) { + return instance; + } + this.passwordHash = null; + this.isHashed = false; + this._initializePassword(); + instance = this; + } + + static getInstance() { + if (!instance) { + instance = new AuthService(); + } + return instance; + } +} +``` + +--- + +## 4. TokenManager 服务 + +TokenManager 负责 JWT Token 的生成和验证。 + +### 4.1 JWT Token 生成 + +```javascript +const jwt = require('jsonwebtoken'); +const TOKEN_EXPIRY = '24h'; + +class TokenManager { + constructor() { + this.secret = this._getSecret(); + } + + _getSecret() { + const jwtSecret = process.env.JWT_SECRET; + if (jwtSecret) { + return jwtSecret; + } + + const password = process.env.REMOTE_SECURITY_PASSWORD; + if (password) { + return password; + } + + const defaultSecret = 'remote-control-default-secret-change-in-production'; + logger.warn('TokenManager using default secret. Please set JWT_SECRET.'); + return defaultSecret; + } + + generateToken(payload) { + const tokenPayload = { + userId: payload.userId || 'default-user', + iat: Math.floor(Date.now() / 1000) + }; + + const options = { + expiresIn: TOKEN_EXPIRY + }; + + try { + const token = jwt.sign(tokenPayload, this.secret, options); + logger.debug('Token generated', { userId: tokenPayload.userId }); + return token; + } catch (error) { + logger.error('Failed to generate token', { error: error.message }); + return null; + } + } +} +``` + +**Token 载荷结构:** + +```json +{ + "userId": "default-user", + "iat": 1699999999, + "exp": 1700086399 +} +``` + +### 4.2 Token 验证 + +```javascript +verifyToken(token) { + if (!token) { + return null; + } + + try { + const decoded = jwt.verify(token, this.secret); + logger.debug('Token verified', { userId: decoded.userId }); + return decoded; + } catch (error) { + if (error.name === 'TokenExpiredError') { + logger.warn('Token expired', { expiredAt: error.expiredAt }); + } else if (error.name === 'JsonWebTokenError') { + logger.warn('Invalid token', { error: error.message }); + } else { + logger.error('Token verification error', { error: error.message }); + } + return null; + } +} + +decodeToken(token) { + if (!token) { + return null; + } + + try { + const decoded = jwt.decode(token); + return decoded; + } catch (error) { + logger.error('Failed to decode token', { error: error.message }); + return null; + } +} +``` + +**验证流程:** + +``` +客户端 Token + │ + ▼ +检查 Token 存在 ──无──→ 返回 null + │ + 有 + │ + ▼ +jwt.verify() + │ + ├─→ 成功:返回解码后的载荷 + │ + └─→ 失败: + ├─ TokenExpiredError: Token 已过期 + ├─ JsonWebTokenError: Token 无效 + └─ 其他错误 + │ + ▼ + 返回 null +``` + +--- + +## 5. ClipboardService 剪贴板服务 + +ClipboardService 实现剪贴板的读写操作,支持文本和图片格式。 + +### 5.1 文本读写 + +**读取剪贴板文本:** + +```javascript +async read() { + return new Promise((resolve, reject) => { + const psCode = ` +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +if ([Windows.Forms.Clipboard]::ContainsText()) { + $text = [Windows.Forms.Clipboard]::GetText() + Write-Output "TYPE:TEXT" + Write-Output $text +} elseif ([Windows.Forms.Clipboard]::ContainsImage()) { + $image = [Windows.Forms.Clipboard]::GetImage() + if ($image -ne $null) { + $ms = New-Object System.IO.MemoryStream + $image.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) + $bytes = $ms.ToArray() + $base64 = [Convert]::ToBase64String($bytes) + Write-Output "TYPE:IMAGE" + Write-Output $base64 + } +} else { + Write-Output "TYPE:EMPTY" +} +`; + + const ps = spawn('powershell', ['-NoProfile', '-Command', psCode]); + let output = ''; + + ps.stdout.on('data', (data) => { + output += data.toString(); + }); + + ps.on('close', () => { + const lines = output.trim().split('\n'); + const typeLine = lines[0]; + + if (typeLine && typeLine.includes('TYPE:TEXT')) { + const text = lines.slice(1).join('\n'); + resolve({ + type: 'text', + data: text, + size: Buffer.byteLength(text, 'utf8') + }); + } else { + resolve({ type: 'empty', data: null, size: 0 }); + } + }); + }); +} +``` + +**写入剪贴板文本:** + +```javascript +async set(content) { + return new Promise((resolve, reject) => { + if (content.type === 'text') { + const escapedText = content.data + .replace(/'/g, "''") // 转义单引号 + .replace(/\r\n/g, '`r`n') // 转义换行符 + .replace(/\n/g, '`n'); + psCode = ` +Add-Type -AssemblyName System.Windows.Forms +[Windows.Forms.Clipboard]::SetText('${escapedText}') +Write-Output "SUCCESS" +`; + } + + const ps = spawn('powershell', ['-NoProfile', '-Command', psCode]); + ps.on('close', () => { + resolve(output.includes('SUCCESS')); + }); + }); +} +``` + +### 5.2 图片读写 + +**读取剪贴板图片:** + +```powershell +if ([Windows.Forms.Clipboard]::ContainsImage()) { + $image = [Windows.Forms.Clipboard]::GetImage() + if ($image -ne $null) { + $ms = New-Object System.IO.MemoryStream + $image.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) + $bytes = $ms.ToArray() + $base64 = [Convert]::ToBase64String($bytes) + Write-Output "TYPE:IMAGE" + Write-Output $base64 + } +} +``` + +**写入剪贴板图片:** + +```javascript +else if (content.type === 'image') { + psCode = ` +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +$bytes = [Convert]::FromBase64String('${content.data}') +$ms = New-Object System.IO.MemoryStream(,$bytes) +$image = [System.Drawing.Image]::FromStream($ms) +[Windows.Forms.Clipboard]::SetImage($image) +Write-Output "SUCCESS" +`; +} +``` + +### 5.3 大小限制 + +服务定义了剪贴板内容大小限制: + +```javascript +const CLIPBOARD_THRESHOLD = 500 * 1024; // 500KB + +class ClipboardService { + constructor() { + this.threshold = CLIPBOARD_THRESHOLD; + } + + isSmallContent(size) { + return size <= this.threshold; + } +} +``` + +**使用示例:** + +```javascript +const clipboard = new ClipboardService(); +const content = await clipboard.read(); + +if (clipboard.isSmallContent(content.size)) { + // 可以通过 WebSocket 发送 + ws.send(JSON.stringify(content)); +} else { + // 内容过大,需要特殊处理 + ws.send(JSON.stringify({ + type: 'error', + message: 'Clipboard content too large' + })); +} +``` + +--- + +## 6. FileService 文件服务 + +FileService 提供文件管理、分块上传和断点续传功能。 + +### 6.1 文件列表 + +```javascript +getFileList() { + try { + const files = fs.readdirSync(this.uploadDir); + return files + .filter(f => { + const filePath = path.join(this.uploadDir, f); + return !fs.statSync(filePath).isDirectory(); + }) + .map(f => { + const filePath = path.join(this.uploadDir, f); + const stat = fs.statSync(filePath); + return { + name: f, + size: stat.size, + modified: stat.mtime, + type: path.extname(f) + }; + }); + } catch (error) { + logger.error('Failed to get file list', { error: error.message }); + return []; + } +} +``` + +**目录浏览:** + +```javascript +browseDirectory(relativePath = '') { + try { + const safePath = path.normalize(relativePath || '').replace(/^(\.\.(\/|\\|$))+/, ''); + const targetDir = path.join(this.uploadDir, safePath); + + // 安全检查:防止路径遍历攻击 + if (!targetDir.startsWith(this.uploadDir)) { + return { error: 'Access denied', items: [], currentPath: '' }; + } + + const items = fs.readdirSync(targetDir).map(name => { + const itemPath = path.join(targetDir, name); + const stat = fs.statSync(itemPath); + const isDirectory = stat.isDirectory(); + + return { + name, + isDirectory, + size: isDirectory ? 0 : stat.size, + modified: stat.mtime, + type: isDirectory ? 'directory' : path.extname(name) + }; + }); + + // 排序:目录优先,然后按名称排序 + items.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + return { + items, + currentPath: safePath, + parentPath: safePath ? path.dirname(safePath) : null + }; + } catch (error) { + return { error: error.message, items: [], currentPath: relativePath }; + } +} +``` + +### 6.2 分块上传 + +**保存文件块:** + +```javascript +saveChunk(fileId, chunkIndex, data) { + try { + const chunkPath = path.join(this.tempDir, `${fileId}.${chunkIndex}`); + fs.writeFileSync(chunkPath, data); + return true; + } catch (error) { + logger.error('Failed to save chunk', { error: error.message }); + return false; + } +} +``` + +**合并文件块:** + +```javascript +mergeChunks(fileId, totalChunks, filename) { + try { + const filePath = path.join(this.uploadDir, path.basename(filename)); + const fd = fs.openSync(filePath, 'w'); + + for (let i = 0; i < totalChunks; i++) { + const chunkPath = path.join(this.tempDir, `${fileId}.${i}`); + if (!fs.existsSync(chunkPath)) { + fs.closeSync(fd); + return false; // 缺少块,合并失败 + } + const chunkData = fs.readFileSync(chunkPath); + fs.writeSync(fd, chunkData, 0, chunkData.length, null); + fs.unlinkSync(chunkPath); // 删除临时块文件 + } + + fs.closeSync(fd); + return true; + } catch (error) { + logger.error('Failed to merge chunks', { error: error.message }); + return false; + } +} +``` + +### 6.3 断点续传 + +**文件流读取(支持 Range 请求):** + +```javascript +getFileStream(filename, range) { + const filePath = this.getFilePath(filename); + if (!filePath) return null; + + const stat = fs.statSync(filePath); + const fileSize = stat.size; + + if (range) { + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; + const chunkSize = end - start + 1; + + return { + stream: fs.createReadStream(filePath, { start, end }), + contentRange: `bytes ${start}-${end}/${fileSize}`, + contentLength: chunkSize, + fileSize + }; + } + + return { + stream: fs.createReadStream(filePath), + contentLength: fileSize, + fileSize + }; +} +``` + +**清理临时块:** + +```javascript +cleanupChunks(fileId) { + try { + const files = fs.readdirSync(this.tempDir); + files.forEach(f => { + if (f.startsWith(fileId + '.')) { + fs.unlinkSync(path.join(this.tempDir, f)); + } + }); + } catch (error) { + logger.error('Failed to cleanup chunks', { error: error.message }); + } +} +``` + +**分块上传流程图:** + +``` +客户端 服务端 + │ │ + │ 1. 初始化上传 │ + │ (fileId, totalChunks) │ + │ ──────────────────────> │ + │ │ + │ 2. 上传块 │ + │ (fileId, index, data) │ + │ ──────────────────────> │ saveChunk() + │ │ + │ ... 重复上传块 ... │ + │ │ + │ 3. 完成上传 │ + │ (fileId, totalChunks, │ + │ filename) │ + │ ──────────────────────> │ mergeChunks() + │ │ + │ 4. 返回结果 │ + │ <────────────────────── │ +``` + +--- + +## 7. FRPService 内网穿透服务 + +FRPService 管理 frpc 客户端进程,实现内网穿透功能。 + +### 7.1 frpc 进程管理 + +```javascript +class FRPService { + constructor(options = {}) { + this.enabled = options.enabled !== false; + this.frpcPath = options.frpcPath || path.join(paths.getFRPPath(), 'frpc.exe'); + this.configPath = options.configPath || path.join(paths.getFRPPath(), 'frpc.toml'); + this.process = null; + this.isRunning = false; + } + + start() { + if (!this.enabled) { + logger.info('FRP service is disabled'); + return; + } + + if (this.isRunning) { + logger.warn('FRP service is already running'); + return; + } + + try { + if (!fs.existsSync(this.frpcPath)) { + logger.error('FRP client not found', { path: this.frpcPath }); + return; + } + + if (!fs.existsSync(this.configPath)) { + logger.error('FRP config not found', { path: this.configPath }); + return; + } + + const runtimeConfigPath = this._prepareConfig(); + + this.process = spawn(this.frpcPath, ['-c', runtimeConfigPath], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + }); + + this.isRunning = true; + + this.process.stdout.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.info(`[FRP] ${output}`); + } + }); + + this.process.stderr.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.error(`[FRP] ${output}`); + } + }); + + this.process.on('close', (code) => { + logger.info('FRP process closed', { code }); + this.isRunning = false; + this.process = null; + }); + + logger.info('FRP service started successfully'); + } catch (error) { + logger.error('Failed to start FRP service', { error: error.message }); + this.isRunning = false; + } + } + + stop() { + if (!this.isRunning || !this.process) { + return; + } + + logger.info('Stopping FRP service'); + + try { + this.process.kill(); + this.process = null; + this.isRunning = false; + logger.info('FRP service stopped'); + } catch (error) { + logger.error('Failed to stop FRP service', { error: error.message }); + } + } +} +``` + +### 7.2 配置处理 + +```javascript +_prepareConfig() { + const frpDir = paths.getFRPPath(); + const logPath = path.join(paths.getBasePath(), 'logs', 'frpc.log'); + const logsDir = path.dirname(logPath); + + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + if (fs.existsSync(this.configPath)) { + let content = fs.readFileSync(this.configPath, 'utf8'); + // 动态替换日志路径 + content = content.replace(/log\.to\s*=\s*"[^"]*"/, `log.to = "${logPath.replace(/\\/g, '\\\\')}"`); + + const tempConfigPath = path.join(frpDir, 'frpc-runtime.toml'); + fs.writeFileSync(tempConfigPath, content); + return tempConfigPath; + } + + return this.configPath; +} +``` + +**获取服务状态:** + +```javascript +getStatus() { + return { + enabled: this.enabled, + running: this.isRunning + }; +} +``` + +--- + +## 8. GiteaService Git 服务 + +GiteaService 管理 Gitea 进程,提供自托管的 Git 服务。 + +### 8.1 gitea 进程管理 + +```javascript +class GiteaService { + constructor(options = {}) { + this.enabled = options.enabled !== false; + this.giteaPath = options.giteaPath || path.join(paths.getBasePath(), 'gitea', 'gitea.exe'); + this.workPath = options.workPath || path.join(paths.getBasePath(), 'gitea'); + this.process = null; + this.isRunning = false; + } + + start() { + if (!this.enabled) { + logger.info('Gitea service is disabled'); + return; + } + + if (this.isRunning) { + logger.warn('Gitea service is already running'); + return; + } + + try { + if (!fs.existsSync(this.giteaPath)) { + logger.error('Gitea executable not found', { path: this.giteaPath }); + return; + } + + this.process = spawn(this.giteaPath, ['web'], { + cwd: this.workPath, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + env: { + ...process.env, + GITEA_WORK_DIR: this.workPath + } + }); + + this.isRunning = true; + + this.process.stdout.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.info(`[Gitea] ${output}`); + } + }); + + this.process.stderr.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.error(`[Gitea] ${output}`); + } + }); + + this.process.on('error', (error) => { + logger.error('Gitea process error', { error: error.message }); + this.isRunning = false; + }); + + this.process.on('close', (code) => { + logger.info('Gitea process closed', { code }); + this.isRunning = false; + this.process = null; + }); + + logger.info('Gitea service started successfully'); + } catch (error) { + logger.error('Failed to start Gitea service', { error: error.message }); + this.isRunning = false; + } + } + + stop() { + if (!this.isRunning || !this.process) { + return; + } + + logger.info('Stopping Gitea service'); + + try { + this.process.kill(); + this.process = null; + this.isRunning = false; + logger.info('Gitea service stopped'); + } catch (error) { + logger.error('Failed to stop Gitea service', { error: error.message }); + } + } + + getStatus() { + return { + enabled: this.enabled, + running: this.isRunning + }; + } +} +``` + +**进程管理要点:** + +| 配置项 | 说明 | +|--------|------| +| `cwd` | 工作目录,Gitea 数据存储位置 | +| `windowsHide` | 隐藏控制台窗口 | +| `GITEA_WORK_DIR` | Gitea 工作目录环境变量 | +| `stdio` | 标准输入忽略,标准输出和错误通过管道捕获 | + +--- + +## 总结 + +本文档涵盖了远程控制系统中八个核心服务的实现细节: + +| 服务 | 主要功能 | 关键技术 | +|------|----------|----------| +| FFmpegEncoder | 视频流编码 | FFmpeg、GDI 捕获、自动重试 | +| PowerShellInput | 远程输入控制 | Windows API、虚拟键码 | +| AuthService | 用户认证 | bcrypt 密码哈希 | +| TokenManager | Token 管理 | JWT | +| ClipboardService | 剪贴板操作 | PowerShell、Base64 编码 | +| FileService | 文件管理 | 分块上传、断点续传 | +| FRPService | 内网穿透 | frpc 进程管理 | +| GiteaService | Git 服务 | Gitea 进程管理 | + +开发新服务时,请参考现有服务的架构模式: + +1. 继承基类服务(如 StreamService、InputService) +2. 实现生命周期方法(start、stop) +3. 使用 logger 记录关键操作 +4. 处理错误和异常情况 +5. 提供状态查询接口 diff --git a/remote/docs/开发/架构设计.md b/remote/docs/开发/架构设计.md new file mode 100644 index 0000000..44c82a3 --- /dev/null +++ b/remote/docs/开发/架构设计.md @@ -0,0 +1,646 @@ +# 架构设计文档 + +## 1. 整体架构说明 + +本项目是一个远程桌面控制服务端应用,采用模块化、事件驱动的架构设计。核心设计理念包括: + +- **依赖注入 (DI)**: 通过容器管理服务生命周期和依赖关系 +- **事件驱动**: 使用事件总线实现模块间松耦合通信 +- **分层架构**: 清晰的职责划分,便于维护和扩展 + +### 1.1 架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 客户端 (Browser) │ +│ ┌─────────────────┬─────────────────┐ │ +│ │ HTTP 请求 │ WebSocket 连接 │ │ +└────────────────────┼─────────────────┼─────────────────┼────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ App.js │ +│ (应用主类 - 启动入口) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Container │ │ EventBus │ │ ErrorHandler │ +│ (依赖注入) │ │ (事件总线) │ │ (错误处理) │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ + │ 服务注册与解析 + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 服务层 (Services) │ +├─────────────┬─────────────┬─────────────┬─────────────┬─────────────────────┤ +│ Auth │ Stream │ Input │ Clipboard │ Network │ +│ 认证服务 │ 流媒体 │ 输入控制 │ 剪贴板 │ FRP/Gitea │ +└─────────────┴─────────────┴─────────────┴─────────────┴─────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 服务器层 (Server) │ +├─────────────────────────────┬───────────────────────────────────────────────┤ +│ HTTP Server │ WebSocket Server │ +│ (静态资源/API路由) │ (实时通信/流传输) │ +├─────────────────────────────┼───────────────────────────────────────────────┤ +│ InputHandler │ StreamBroadcaster │ +│ (输入处理) │ (流广播) │ +└─────────────────────────────┴───────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 系统层 (System) │ +├─────────────────────────────┬───────────────────────────────────────────────┤ +│ FFmpeg Encoder │ PowerShell Input │ +│ (视频编码) │ (系统输入模拟) │ +└─────────────────────────────┴───────────────────────────────────────────────┘ +``` + +### 1.2 启动流程时序图 + +``` +┌──────┐ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ +│ App │ │Container│ │ErrorHandler│ │HTTPServer│ │WSServer │ +└──┬───┘ └────┬────┘ └─────┬─────┘ └────┬─────┘ └────┬────┘ + │ │ │ │ │ + │ bootstrap() │ │ │ │ + │─────────────>│ │ │ │ + │ │ │ │ │ + │ register services │ │ │ + │─────────────────────────────>│ │ │ + │ │ │ │ │ + │ start() │ │ │ │ + │──────────────┼───────────────┼───────────────┼───────────────┤ + │ │ │ │ │ + │ │ initialize() │ │ │ + │ │──────────────>│ │ │ + │ │ │ │ │ + │ │ │ │ start() │ + │ │ │ │──────────────>│ + │ │ │ │ │ + │ │ │ │ start() │ + │ │ │──────────────>│ │ + │ │ │ │ │ + │ emit(APP_START) │ │ │ + │───────────────────────────────────────────────────────────────── + │ │ │ │ │ +``` + +--- + +## 2. 核心模块说明 + +### 2.1 App.js - 应用主类 + +应用主类负责整个应用的生命周期管理,包括启动、运行和关闭。 + +#### 职责 + +- **引导启动 (Bootstrap)**: 初始化容器和事件总线,注册所有服务 +- **启动服务 (Start)**: 按顺序启动所有已注册的服务 +- **优雅关闭 (Graceful Shutdown)**: 处理系统信号,安全停止所有服务 + +#### 启动流程 + +```javascript +const App = require('./core/App'); + +const app = new App(); + +await app.bootstrap(); +await app.start(); +``` + +#### 服务注册 + +App.js 通过 Container 注册以下服务: + +| 服务名称 | 类/模块 | 职责 | +|---------|--------|------| +| `config` | config | 配置管理 | +| `logger` | logger | 日志记录 | +| `errorHandler` | ErrorHandler | 全局错误处理 | +| `authService` | AuthService | 用户认证 | +| `tokenManager` | TokenManager | JWT Token 管理 | +| `ffmpegEncoder` | FFmpegEncoder | 视频流编码 | +| `inputService` | PowerShellInput | 输入控制 | +| `inputHandler` | InputHandler | 输入消息处理 | +| `clipboardService` | ClipboardService | 剪贴板操作 | +| `fileService` | FileService | 文件操作 | +| `frpService` | FRPService | 内网穿透 | +| `giteaService` | GiteaService | Git 服务 | +| `httpServer` | Server | HTTP 服务器 | +| `wsServer` | WebSocketServer | WebSocket 服务器 | +| `streamBroadcaster` | StreamBroadcaster | 流广播 | + +#### 启动顺序 + +```javascript +async start() { + errorHandler.initialize(); + this._setupRoutes(); + + await httpServer.start(); + wsServer.start(httpServer.getHTTPServer()); + this._setupWebSocketHandlers(); + + ffmpegEncoder.start(); + streamBroadcaster.setEncoder(ffmpegEncoder); + + await inputService.start(); + frpService.start(); + giteaService.start(); + + await this.eventBus.emit(EventTypes.APP_START, { ... }); + this._setupGracefulShutdown(); +} +``` + +--- + +### 2.2 Container.js - 依赖注入容器 + +Container 是一个轻量级的依赖注入容器,负责服务的注册、解析和生命周期管理。 + +#### 核心功能 + +- **服务注册**: 使用工厂函数注册服务 +- **单例模式**: 默认所有服务为单例 +- **循环依赖检测**: 自动检测并阻止循环依赖 + +#### API 说明 + +```javascript +class Container { + register(name, factory, isSingleton = true) + resolve(name) + has(name) + unregister(name) + clear() +} +``` + +#### 使用示例 + +```javascript +const container = new Container(); + +container.register('config', (c) => { + return require('./config'); +}); + +container.register('database', (c) => { + const config = c.resolve('config'); + return new Database(config.db); +}); + +container.register('userService', (c) => { + const db = c.resolve('database'); + return new UserService(db); +}); + +const userService = container.resolve('userService'); +``` + +#### 循环依赖检测 + +```javascript +resolve(name) { + if (this._resolving.has(name)) { + const chain = Array.from(this._resolving).join(' -> '); + throw new Error( + `Circular dependency detected: ${chain} -> ${name}` + ); + } + + this._resolving.add(name); + try { + const instance = service.factory(this); + return instance; + } finally { + this._resolving.delete(name); + } +} +``` + +--- + +### 2.3 EventBus.js - 事件总线 + +EventBus 实现了发布-订阅模式,用于模块间的松耦合通信。 + +#### 核心功能 + +- **事件订阅**: `on(event, handler)` 订阅事件 +- **事件发布**: `emit(event, data)` 发布事件 +- **一次性订阅**: `once(event, handler)` 只触发一次 +- **取消订阅**: `off(event, handler)` 移除监听器 + +#### API 说明 + +```javascript +class EventBus { + on(event, handler) + off(event, handler) + emit(event, data) + once(event, handler) + removeAllListeners(event) + listenerCount(event) +} +``` + +#### 使用示例 + +```javascript +const eventBus = new EventBus(); + +eventBus.on('user:login', async (data) => { + console.log(`User ${data.userId} logged in`); +}); + +await eventBus.emit('user:login', { userId: '123' }); +``` + +#### 事件类型 + +系统预定义的事件类型: + +```javascript +const EventTypes = { + STREAM_START: 'stream:start', + STREAM_STOP: 'stream:stop', + STREAM_DATA: 'stream:data', + STREAM_ERROR: 'stream:error', + CLIENT_CONNECTED: 'client:connected', + CLIENT_DISCONNECTED: 'client:disconnected', + INPUT_EVENT: 'input:event', + APP_START: 'app:start', + APP_STOP: 'app:stop', + ERROR: 'error' +}; +``` + +--- + +### 2.4 ErrorHandler.js - 错误处理器 + +ErrorHandler 提供全局错误处理机制,捕获未处理的异常和 Promise 拒绝。 + +#### 核心功能 + +- **全局异常捕获**: 捕获 `uncaughtException` +- **Promise 拒绝捕获**: 捕获 `unhandledRejection` +- **错误响应生成**: 统一的错误响应格式 + +#### 初始化 + +```javascript +class ErrorHandler { + initialize() { + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', { + message: error.message, + stack: error.stack, + name: error.name + }); + setTimeout(() => process.exit(1), 1000); + }); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection:', { reason }); + }); + } +} +``` + +#### 错误响应格式 + +```javascript +function createErrorResponse(error, code, details) { + const response = { + error: error instanceof Error ? error.message : String(error) + }; + if (code) response.code = code; + if (details !== undefined) response.details = details; + return response; +} +``` + +--- + +## 3. 服务层设计说明 + +### 3.1 服务模块结构 + +``` +src/services/ +├── auth/ # 认证服务 +│ ├── AuthService.js # 认证逻辑 +│ └── TokenManager.js # JWT Token 管理 +├── clipboard/ # 剪贴板服务 +│ └── ClipboardService.js +├── file/ # 文件服务 +│ └── FileService.js +├── input/ # 输入控制服务 +│ ├── InputService.js # 输入接口 +│ └── PowerShellInput.js # PowerShell 实现 +├── network/ # 网络服务 +│ ├── FRPService.js # 内网穿透 +│ └── GiteaService.js # Git 服务 +└── stream/ # 流媒体服务 + ├── FFmpegEncoder.js # FFmpeg 编码 + ├── ScreenCapture.js # 屏幕捕获 + └── StreamService.js # 流管理 +``` + +### 3.2 各服务职责 + +| 服务模块 | 职责 | 关键方法 | +|---------|------|---------| +| **AuthService** | 用户密码验证 | `authenticate(password)` | +| **TokenManager** | JWT 生成与验证 | `generateToken()`, `verifyToken()` | +| **FFmpegEncoder** | 屏幕录制与编码 | `start()`, `stop()`, `getScreenResolution()` | +| **PowerShellInput** | 模拟鼠标键盘输入 | `mouseMove()`, `mouseDown()`, `keyDown()` | +| **ClipboardService** | 剪贴板读写 | `get()`, `set()` | +| **FileService** | 文件系统操作 | `readFile()`, `writeFile()`, `listDir()` | +| **FRPService** | 内网穿透代理 | `start()`, `stop()` | +| **GiteaService** | Git 仓库管理 | `start()`, `stop()` | + +--- + +## 4. 数据流向说明 + +### 4.1 用户认证流程 + +``` +┌────────┐ ┌───────────┐ ┌────────────┐ ┌──────────────┐ +│ Browser│ │HTTPServer │ │AuthService │ │TokenManager │ +└───┬────┘ └─────┬─────┘ └─────┬──────┘ └──────┬───────┘ + │ │ │ │ + │ POST /login │ │ │ + │ {password: xxx} │ │ │ + │────────────────>│ │ │ + │ │ │ │ + │ │ authenticate() │ │ + │ │─────────────────>│ │ + │ │ │ │ + │ │ true/false │ │ + │ │<─────────────────│ │ + │ │ │ │ + │ │ generateToken() │ + │ │────────────────────────────────────────> + │ │ │ │ + │ │ token │ + │ │<──────────────────────────────────────── + │ │ │ │ + │ Set-Cookie: auth=token; redirect / │ │ + │<────────────────│ │ │ + │ │ │ │ +``` + +### 4.2 实时控制流程 + +``` +┌────────┐ ┌──────────┐ ┌─────────────┐ ┌────────────────┐ +│ Browser│ │WSServer │ │InputHandler │ │PowerShellInput │ +└───┬────┘ └────┬─────┘ └──────┬──────┘ └───────┬────────┘ + │ │ │ │ + │ WebSocket │ │ │ + │ Connection │ │ │ + │─────────────>│ │ │ + │ │ │ │ + │ {type: │ │ │ + │ "mouseMove",│ │ │ + │ x: 100, │ │ │ + │ y: 200} │ │ │ + │─────────────>│ │ │ + │ │ │ │ + │ │ handleMessage() │ │ + │ │────────────────>│ │ + │ │ │ │ + │ │ │ mouseMove(100,200)│ + │ │ │──────────────────>│ + │ │ │ │ + │ │ │ 执行系统命令 │ + │ │ │ │ +``` + +### 4.3 视频流传输流程 + +``` +┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ ┌────────┐ +│ FFmpegEncoder│ │StreamBroadcaster│ │ WebSocketServer │ │ Browser│ +└──────┬───────┘ └───────┬───────┘ └────────┬─────────┘ └───┬────┘ + │ │ │ │ + │ 屏幕捕获 + 编码 │ │ │ + │ │ │ │ + │ video data │ │ │ + │───────────────────>│ │ │ + │ │ │ │ + │ │ broadcast to all │ │ + │ │ clients │ │ + │ │────────────────────>│ │ + │ │ │ │ + │ │ │ binary frame │ + │ │ │─────────────────>│ + │ │ │ │ + │ │ │ 渲染视频 │ + │ │ │ │ +``` + +--- + +## 5. WebSocket 消息协议说明 + +### 5.1 消息格式 + +所有 WebSocket 消息使用 JSON 格式: + +```javascript +{ + "type": "messageType", + ...additionalFields +} +``` + +### 5.2 消息类型定义 + +```javascript +const MessageTypes = { + SCREEN_INFO: 'screenInfo', + MOUSE_MOVE: 'mouseMove', + MOUSE_DOWN: 'mouseDown', + MOUSE_UP: 'mouseUp', + MOUSE_WHEEL: 'mouseWheel', + KEY_DOWN: 'keyDown', + KEY_UP: 'keyUp', + CLIPBOARD_GET: 'clipboardGet', + CLIPBOARD_SET: 'clipboardSet', + CLIPBOARD_DATA: 'clipboardData', + CLIPBOARD_RESULT: 'clipboardResult', + CLIPBOARD_TOO_LARGE: 'clipboardTooLarge' +}; +``` + +### 5.3 消息详细说明 + +#### 屏幕信息 (screenInfo) + +服务端发送屏幕分辨率信息: + +```javascript +{ + "type": "screenInfo", + "width": 1920, + "height": 1080 +} +``` + +#### 鼠标移动 (mouseMove) + +```javascript +{ + "type": "mouseMove", + "x": 100, + "y": 200 +} +``` + +#### 鼠标按下/释放 (mouseDown/mouseUp) + +```javascript +{ + "type": "mouseDown", + "button": 0 +} +``` + +| button 值 | 说明 | +|----------|------| +| 0 | 左键 | +| 1 | 中键 | +| 2 | 右键 | + +#### 鼠标滚轮 (mouseWheel) + +```javascript +{ + "type": "mouseWheel", + "deltaX": 0, + "deltaY": -120 +} +``` + +#### 键盘按下/释放 (keyDown/keyUp) + +```javascript +{ + "type": "keyDown", + "keyCode": 65, + "key": "a" +} +``` + +#### 剪贴板获取 (clipboardGet) + +客户端请求获取剪贴板内容: + +```javascript +{ + "type": "clipboardGet" +} +``` + +#### 剪贴板设置 (clipboardSet) + +客户端设置剪贴板内容: + +```javascript +{ + "type": "clipboardSet", + "data": "clipboard text content" +} +``` + +#### 剪贴板数据 (clipboardData) + +服务端返回剪贴板数据: + +```javascript +{ + "type": "clipboardData", + "data": "clipboard text content" +} +``` + +#### 剪贴板操作结果 (clipboardResult) + +```javascript +{ + "type": "clipboardResult", + "success": true +} +``` + +#### 剪贴板过大 (clipboardTooLarge) + +```javascript +{ + "type": "clipboardTooLarge", + "size": 10485760, + "maxSize": 5242880 +} +``` + +### 5.4 消息处理流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ WebSocket 消息处理 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 客户端消息 ──> JSON 解析 ──> 类型判断 ──> 分发处理 ──> 执行操作 │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ InputHandler │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ mouseMove ──> inputService.mouseMove(x, y) │ │ +│ │ mouseDown ──> inputService.mouseDown(button) │ │ +│ │ mouseUp ──> inputService.mouseUp(button) │ │ +│ │ mouseWheel ──> inputService.mouseWheel(deltaX, deltaY) │ │ +│ │ keyDown ──> inputService.keyDown(keyCode) │ │ +│ │ keyUp ──> inputService.keyUp(keyCode) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ ClipboardService │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ clipboardGet ──> clipboardService.get() ──> 返回数据 │ │ +│ │ clipboardSet ──> clipboardService.set(data) ──> 确认 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 6. 设计原则总结 + +### 6.1 SOLID 原则应用 + +| 原则 | 应用场景 | +|------|---------| +| **单一职责** | 每个服务类只负责一个特定功能 | +| **开放封闭** | 通过 Container 注册新服务,无需修改核心代码 | +| **依赖倒置** | 高层模块依赖抽象接口,具体实现通过 Container 注入 | + +### 6.2 最佳实践 + +1. **依赖注入**: 所有服务通过 Container 获取,避免硬编码依赖 +2. **事件驱动**: 模块间通过 EventBus 通信,降低耦合度 +3. **优雅关闭**: 正确处理系统信号,确保资源释放 +4. **错误处理**: 全局捕获未处理异常,记录日志并安全退出 +5. **日志记录**: 统一的日志格式,便于问题排查 diff --git a/remote/docs/开发/部署指南.md b/remote/docs/开发/部署指南.md new file mode 100644 index 0000000..c3a94d3 --- /dev/null +++ b/remote/docs/开发/部署指南.md @@ -0,0 +1,240 @@ +# 部署指南 + +本文档介绍远程屏幕监控系统的开发环境运行、生产环境部署、pkg 打包和 Windows 服务安装。 + +--- + +## 一、开发环境运行 + +### 1.1 环境要求 + +| 软件 | 版本要求 | 说明 | +|------|----------|------| +| Node.js | >= 16.0.0 | 推荐使用 LTS 版本 | +| 操作系统 | Windows | 输入控制功能需要 Windows 系统 | +| FFmpeg | 内置 | 通过 npm 自动安装 | + +### 1.2 安装依赖 + +```powershell +# 进入项目目录 +cd C:\Users\xuanchi\Desktop\remote + +# 安装依赖 +npm install +``` + +### 1.3 启动开发服务器 + +```powershell +# 方式一:使用 dev 脚本 +npm run dev + +# 方式二:使用 start 脚本 +npm start + +# 方式三:直接运行 +node src/index.js +``` + +启动成功后,访问 http://localhost:3000 查看屏幕流。 + +--- + +## 二、生产环境部署 + +### 2.1 环境变量配置 + +所有配置项都可以通过环境变量覆盖,格式为 `REMOTE_
_`: + +```powershell +# 服务器配置 +REMOTE_SERVER_PORT=3000 +REMOTE_SERVER_HOST=0.0.0.0 + +# 安全配置 +REMOTE_SECURITY_PASSWORD=your_password +REMOTE_SECURITY_TOKENEXPIRY=3600 + +# JWT 密钥 +JWT_SECRET=your_jwt_secret + +# 流媒体配置 +REMOTE_STREAM_FPS=30 +REMOTE_STREAM_BITRATE=4000k + +# 输入控制配置 +REMOTE_INPUT_MOUSEENABLED=true +REMOTE_INPUT_KEYBOARDENABLED=true + +# FRP 配置 +REMOTE_FRP_ENABLED=true +``` + +### 2.2 配置文件修改 + +配置文件位于 `config/default.json`: + +```json +{ + "server": { + "port": 3000, + "host": "0.0.0.0" + }, + "stream": { + "fps": 30, + "bitrate": "4000k", + "gop": 10, + "preset": "ultrafast", + "resolution": { + "width": 1920, + "height": 1080 + } + }, + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + }, + "security": { + "password": "", + "tokenExpiry": 3600 + }, + "frp": { + "enabled": true, + "frpcPath": "./frp/frpc.exe", + "configPath": "./frp/frpc.toml" + }, + "gitea": { + "enabled": true + } +} +``` + +### 2.3 启动生产服务 + +```powershell +# 设置环境变量 +$env:NODE_ENV="production" +$env:REMOTE_SECURITY_PASSWORD="your_password" + +# 启动服务 +node src/index.js +``` + +或创建启动脚本 `start.ps1`: + +```powershell +$env:NODE_ENV="production" +$env:REMOTE_SECURITY_PASSWORD="your_password" +$env:JWT_SECRET="your_jwt_secret" + +node src/index.js +``` + +--- + +## 三、pkg 打包说明 + +### 3.1 package.json 中的 pkg 配置 + +```json +{ + "pkg": { + "assets": [ + "node_modules/@ffmpeg-installer/**/*" + ], + "scripts": [ + "src/**/*.js" + ] + } +} +``` + +| 配置项 | 说明 | +|--------|------| +| `assets` | 需要打包的资源文件,包含 FFmpeg 二进制文件 | +| `scripts` | 需要打包的脚本文件 | + +### 3.2 打包命令 + +```powershell +# 执行打包 +npm run build +``` + +打包命令实际执行: + +```powershell +# 1. 使用 pkg 打包为可执行文件 +pkg . --targets node18-win-x64 --output dist/remote-screen-monitor.exe + +# 2. 复制资源文件 +npm run build:copy-assets +``` + +### 3.3 打包输出目录 + +打包后的文件位于 `dist/` 目录: + +``` +dist/ +├── remote-screen-monitor.exe # 主程序 +├── public/ # 前端静态文件 +│ ├── css/ +│ ├── js/ +│ └── index.html +├── config/ # 配置文件 +│ └── default.json +├── frp/ # FRP 客户端 +│ ├── frpc.exe +│ └── frpc.toml +└── ffmpeg.exe # FFmpeg 编码器 +``` + +### 3.4 资源文件处理 + +打包时需要手动复制的资源: + +| 资源 | 来源 | 目标 | +|------|------|------| +| 前端文件 | `public/` | `dist/public/` | +| 配置文件 | `config/` | `dist/config/` | +| FRP 客户端 | `frp/` | `dist/frp/` | +| FFmpeg | `node_modules/@ffmpeg-installer/win32-x64/ffmpeg.exe` | `dist/ffmpeg.exe` | + +--- + +## 四、NSSM Windows 服务安装 + +### 4.1 服务安装命令 + +```powershell +# 以管理员身份运行 PowerShell +cd C:\Users\xuanchi\Desktop\remote\nssm + +# 安装服务(使用打包后的 exe) +.\nssm.exe install RemoteApp "C:\path\to\dist\remote-screen-monitor.exe" + +# 或使用 Node.js 运行 +.\nssm.exe install RemoteApp "C:\Program Files\nodejs\node.exe" "C:\Users\xuanchi\Desktop\remote\src\index.js" +``` + +### 4.2 服务配置 + +```powershell +# 设置工作目录 +.\nssm.exe set RemoteApp AppDirectory "C:\path\to\app" + +# 设置显示名称 +.\nssm.exe set RemoteApp DisplayName "Remote Desktop Application" + +# 设置描述 +.\nssm.exe set RemoteApp Description "Remote screen streaming service" + +# 设置启动类型(自动启动) +.\nssm.exe set RemoteApp Start SERVICE_AUTO_START + +# 设置日志输出 +.\nssm.exe set RemoteApp AppStdout "C:\path\to\app\logs\service.log" +.\nssm.exe set RemoteApp AppStderr "C:\path\to\ \ No newline at end of file diff --git a/remote/docs/开发/配置指南.md b/remote/docs/开发/配置指南.md new file mode 100644 index 0000000..ebad026 --- /dev/null +++ b/remote/docs/开发/配置指南.md @@ -0,0 +1,569 @@ +# 配置指南 + +本文档详细说明 Remote 项目的配置系统,包括配置文件结构、配置项说明、环境变量覆盖机制以及配置验证规则。 + +## 目录 + +- [配置文件结构](#配置文件结构) +- [配置项详细说明](#配置项详细说明) + - [server 配置](#server-配置) + - [stream 配置](#stream-配置) + - [input 配置](#input-配置) + - [security 配置](#security-配置) + - [frp 配置](#frp-配置) + - [gitea 配置](#gitea-配置) +- [环境变量覆盖机制](#环境变量覆盖机制) +- [配置验证规则](#配置验证规则) +- [不同环境配置示例](#不同环境配置示例) + +--- + +## 配置文件结构 + +配置文件位于 `config/default.json`,采用 JSON 格式,包含以下顶级配置节: + +```json +{ + "server": { ... }, + "stream": { ... }, + "input": { ... }, + "security": { ... }, + "frp": { ... }, + "gitea": { ... } +} +``` + +配置加载优先级(从低到高): + +1. **默认值** - `schema.js` 中定义的 `defaultConfig` +2. **配置文件** - `config/default.json` +3. **环境变量** - `REMOTE_
_` 格式的环境变量 + +--- + +## 配置项详细说明 + +### server 配置 + +服务器基本配置,控制 HTTP 服务的监听地址和端口。 + +| 配置项 | 类型 | 默认值 | 必填 | 说明 | +|--------|------|--------|------|------| +| `port` | number | 3000 | 是 | 服务监听端口,范围 1-65535 | +| `host` | string | "0.0.0.0" | 是 | 服务监听地址,`0.0.0.0` 表示监听所有网卡 | + +**配置示例:** + +```json +{ + "server": { + "port": 3000, + "host": "0.0.0.0" + } +} +``` + +**使用场景:** + +- 开发环境:使用默认端口 3000 +- 生产环境:可使用 80 或 443(需要管理员权限) +- 局域网访问:`host` 设置为 `0.0.0.0` +- 仅本机访问:`host` 设置为 `127.0.0.1` + +--- + +### stream 配置 + +视频流编码配置,控制远程桌面的画面质量和性能。 + +| 配置项 | 类型 | 默认值 | 必填 | 约束 | 说明 | +|--------|------|--------|------|------|------| +| `fps` | number | 30 | 是 | 1-120 | 帧率,每秒传输帧数 | +| `bitrate` | string | "4000k" | 是 | - | 码率,控制视频质量,如 `2000k`、`4000k`、`8000k` | +| `gop` | number | 10 | 是 | ≥1 | GOP 大小(关键帧间隔),影响延迟和压缩效率 | +| `preset` | string | "ultrafast" | 是 | - | 编码预设,影响编码速度和压缩效率 | +| `resolution.width` | number | 1920 | 是 | ≥1 | 视频宽度(像素) | +| `resolution.height` | number | 1080 | 是 | ≥1 | 视频高度(像素) | + +**编码预设说明:** + +| 预设值 | 编码速度 | 压缩效率 | 适用场景 | +|--------|----------|----------|----------| +| `ultrafast` | 最快 | 最低 | 低延迟场景,CPU 性能有限 | +| `superfast` | 很快 | 较低 | 实时流媒体 | +| `veryfast` | 快 | 一般 | 平衡性能和质量 | +| `faster` | 较快 | 较好 | 一般用途 | +| `fast` | 中等偏快 | 好 | 推荐设置 | +| `medium` | 中等 | 很好 | 默认推荐 | +| `slow` | 慢 | 很好 | 离线编码 | +| `slower` | 很慢 | 极好 | 高质量要求 | +| `veryslow` | 最慢 | 最好 | 最高质量,非实时 | + +**配置示例:** + +```json +{ + "stream": { + "fps": 30, + "bitrate": "4000k", + "gop": 10, + "preset": "ultrafast", + "resolution": { + "width": 1920, + "height": 1080 + } + } +} +``` + +**性能调优建议:** + +- **低带宽环境**:降低 `bitrate` 至 `2000k`,降低 `resolution` 至 1280x720 +- **高帧率需求**:提高 `fps` 至 60,同时提高 `bitrate` +- **低延迟优先**:使用 `ultrafast` 预设,减小 `gop` 值 +- **画质优先**:使用 `medium` 或 `slow` 预设,提高 `bitrate` + +--- + +### input 配置 + +输入设备控制配置,管理鼠标和键盘的远程输入。 + +| 配置项 | 类型 | 默认值 | 必填 | 约束 | 说明 | +|--------|------|--------|------|------|------| +| `mouseEnabled` | boolean | true | 是 | - | 是否启用鼠标控制 | +| `keyboardEnabled` | boolean | true | 是 | - | 是否启用键盘控制 | +| `sensitivity` | number | 1.0 | 是 | 0.1-10 | 鼠标灵敏度,1.0 为标准灵敏度 | + +**配置示例:** + +```json +{ + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + } +} +``` + +**使用场景:** + +- **演示模式**:禁用鼠标和键盘输入,仅允许观看 +- **高精度操作**:降低灵敏度至 0.5 +- **快速操作**:提高灵敏度至 2.0 + +--- + +### security 配置 + +安全认证配置,控制访问权限和会话管理。 + +| 配置项 | 类型 | 默认值 | 必填 | 约束 | 说明 | +|--------|------|--------|------|------|------| +| `password` | string | "" | 否 | - | 访问密码,为空表示无需密码 | +| `tokenExpiry` | number | 3600 | 是 | ≥60 | Token 有效期(秒),默认 1 小时 | + +**配置示例:** + +```json +{ + "security": { + "password": "your-secure-password", + "tokenExpiry": 3600 + } +} +``` + +**安全建议:** + +- 生产环境务必设置强密码 +- Token 有效期建议设置为 1800-7200 秒 +- 敏感环境可缩短 Token 有效期至 900 秒 + +--- + +### frp 配置 + +FRP 内网穿透配置,用于通过公网访问内网服务。 + +| 配置项 | 类型 | 默认值 | 必填 | 说明 | +|--------|------|--------|------|------| +| `enabled` | boolean | true | 是 | 是否启用 FRP 功能 | +| `frpcPath` | string | "./frp/frpc.exe" | 否 | frpc 客户端路径 | +| `configPath` | string | "./frp/frpc.toml" | 否 | frpc 配置文件路径 | + +**配置示例:** + +```json +{ + "frp": { + "enabled": true, + "frpcPath": "./frp/frpc.exe", + "configPath": "./frp/frpc.toml" + } +} +``` + +--- + +### gitea 配置 + +Gitea 集成配置,用于代码仓库管理。 + +| 配置项 | 类型 | 默认值 | 必填 | 说明 | +|--------|------|--------|------|------| +| `enabled` | boolean | true | 是 | 是否启用 Gitea 集成 | + +**配置示例:** + +```json +{ + "gitea": { + "enabled": true + } +} +``` + +--- + +## 环境变量覆盖机制 + +配置系统支持通过环境变量覆盖配置文件中的值,环境变量优先级最高。 + +### 命名规则 + +环境变量采用 `REMOTE_
_` 格式: + +``` +REMOTE_<配置节>_<配置项> +``` + +- 所有字母大写 +- 使用下划线 `_` 分隔 +- 支持多层嵌套配置 + +### 类型自动转换 + +系统会自动识别并转换环境变量的类型: + +| 环境变量值 | 转换结果 | +|------------|----------| +| `"true"` | `true` (boolean) | +| `"false"` | `false` (boolean) | +| `"123"` | `123` (number) | +| `"123.45"` | `123.45` (number) | +| 其他值 | 原始字符串 | + +### 环境变量示例 + +| 环境变量 | 对应配置 | 说明 | +|----------|----------|------| +| `REMOTE_SERVER_PORT=8080` | `server.port = 8080` | 修改服务端口 | +| `REMOTE_SERVER_HOST=127.0.0.1` | `server.host = "127.0.0.1"` | 修改监听地址 | +| `REMOTE_STREAM_FPS=60` | `stream.fps = 60` | 修改帧率 | +| `REMOTE_STREAM_BITRATE=8000k` | `stream.bitrate = "8000k"` | 修改码率 | +| `REMOTE_INPUT_MOUSEENABLED=false` | `input.mouseEnabled = false` | 禁用鼠标 | +| `REMOTE_INPUT_SENSITIVITY=2.0` | `input.sensitivity = 2.0` | 修改灵敏度 | +| `REMOTE_SECURITY_PASSWORD=secret123` | `security.password = "secret123"` | 设置密码 | +| `REMOTE_SECURITY_TOKENEXPIRY=7200` | `security.tokenExpiry = 7200` | Token 有效期 2 小时 | +| `REMOTE_FRP_ENABLED=false` | `frp.enabled = false` | 禁用 FRP | +| `REMOTE_GITEA_ENABLED=false` | `gitea.enabled = false` | 禁用 Gitea | + +### 嵌套配置的环境变量 + +对于嵌套配置(如 `resolution.width`),使用下划线连接: + +```bash +REMOTE_STREAM_RESOLUTION_WIDTH=1280 +REMOTE_STREAM_RESOLUTION_HEIGHT=720 +``` + +对应配置: + +```json +{ + "stream": { + "resolution": { + "width": 1280, + "height": 720 + } + } +} +``` + +--- + +## 配置验证规则 + +配置系统通过 `schema.js` 定义验证规则,在加载配置时自动执行验证。 + +### 验证流程 + +``` +加载配置 → 合并默认值 → 应用环境变量 → 执行验证 → 返回验证结果 +``` + +### 验证规则详解 + +#### 类型验证 + +| 类型 | 验证规则 | +|------|----------| +| `number` | 必须是有效数字,非 NaN | +| `string` | 必须是字符串类型 | +| `boolean` | 必须是布尔类型 | +| `object` | 必须是非 null 对象,非数组 | + +#### 数值约束 + +| 约束 | 说明 | +|------|------| +| `min` | 最小值(包含) | +| `max` | 最大值(包含) | + +#### 必填验证 + +| 属性 | 说明 | +|------|------| +| `required: true` | 必须提供该配置项 | +| `required: false` | 可选配置项 | + +### 完整验证规则表 + +| 配置项 | 类型 | 必填 | 最小值 | 最大值 | +|--------|------|------|--------|--------| +| `server.port` | number | 是 | 1 | 65535 | +| `server.host` | string | 是 | - | - | +| `stream.fps` | number | 是 | 1 | 120 | +| `stream.bitrate` | string | 是 | - | - | +| `stream.gop` | number | 是 | 1 | - | +| `stream.preset` | string | 是 | - | - | +| `stream.resolution.width` | number | 是 | 1 | - | +| `stream.resolution.height` | number | 是 | 1 | - | +| `input.mouseEnabled` | boolean | 是 | - | - | +| `input.keyboardEnabled` | boolean | 是 | - | - | +| `input.sensitivity` | number | 是 | 0.1 | 10 | +| `security.password` | string | 否 | - | - | +| `security.tokenExpiry` | number | 是 | 60 | - | +| `frp.enabled` | boolean | 是 | - | - | +| `gitea.enabled` | boolean | 是 | - | - | + +### 验证错误处理 + +当配置验证失败时,系统会: + +1. 收集所有验证错误 +2. 输出警告日志 +3. 继续使用配置(不会阻止启动) + +**验证错误示例:** + +```javascript +// 配置验证警告输出示例 +Config validation warnings: [ + 'stream.fps: value 150 is greater than maximum 120', + 'input.sensitivity: value 0.05 is less than minimum 0.1', + 'server.port: expected type number, got string' +] +``` + +--- + +## 不同环境配置示例 + +### 开发环境配置 + +适用于本地开发和测试,注重调试便利性。 + +```json +{ + "server": { + "port": 3000, + "host": "127.0.0.1" + }, + "stream": { + "fps": 30, + "bitrate": "2000k", + "gop": 10, + "preset": "ultrafast", + "resolution": { + "width": 1280, + "height": 720 + } + }, + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + }, + "security": { + "password": "", + "tokenExpiry": 86400 + }, + "frp": { + "enabled": false + }, + "gitea": { + "enabled": false + } +} +``` + +**开发环境特点:** + +- 仅本机访问(`127.0.0.1`) +- 较低码率和分辨率,节省资源 +- 无密码验证,便于快速测试 +- 禁用 FRP 和 Gitea,减少依赖 + +### 生产环境配置 + +适用于正式部署,注重安全性和性能。 + +```json +{ + "server": { + "port": 443, + "host": "0.0.0.0" + }, + "stream": { + "fps": 60, + "bitrate": "8000k", + "gop": 30, + "preset": "fast", + "resolution": { + "width": 1920, + "height": 1080 + } + }, + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + }, + "security": { + "password": "your-strong-password-here", + "tokenExpiry": 1800 + }, + "frp": { + "enabled": true, + "frpcPath": "/opt/frp/frpc", + "configPath": "/etc/frp/frpc.toml" + }, + "gitea": { + "enabled": true + } +} +``` + +**生产环境特点:** + +- 监听所有网卡,支持外网访问 +- 高帧率和高码率,保证画质 +- 强密码保护,短 Token 有效期 +- 启用 FRP 内网穿透和 Gitea 集成 + +### 低带宽环境配置 + +适用于网络带宽受限的场景。 + +```json +{ + "server": { + "port": 3000, + "host": "0.0.0.0" + }, + "stream": { + "fps": 15, + "bitrate": "1000k", + "gop": 30, + "preset": "ultrafast", + "resolution": { + "width": 854, + "height": 480 + } + }, + "input": { + "mouseEnabled": true, + "keyboardEnabled": true, + "sensitivity": 1.0 + }, + "security": { + "password": "secure-password", + "tokenExpiry": 3600 + }, + "frp": { + "enabled": true + }, + "gitea": { + "enabled": false + } +} +``` + +**低带宽环境特点:** + +- 低帧率(15fps)和低码率(1000k) +- 较小分辨率(480p) +- 使用 `ultrafast` 预设减少编码延迟 +- 较大的 GOP 值提高压缩效率 + +### 环境变量配置示例 + +通过环境变量快速切换配置: + +```bash +# Windows (PowerShell) +$env:REMOTE_SERVER_PORT=8080 +$env:REMOTE_STREAM_FPS=60 +$env:REMOTE_SECURITY_PASSWORD="production-password" + +# Linux/macOS +export REMOTE_SERVER_PORT=8080 +export REMOTE_STREAM_FPS=60 +export REMOTE_SECURITY_PASSWORD="production-password" +``` + +--- + +## 配置 API + +在代码中使用配置: + +```javascript +const config = require('./config'); + +// 获取整个配置 +const allConfig = config.getAll(); + +// 获取特定配置节 +const serverConfig = config.getSection('server'); + +// 获取单个配置项(支持点号分隔的路径) +const port = config.get('server.port'); +const width = config.get('stream.resolution.width', 1920); + +// 重新加载配置 +config.reload(); +``` + +--- + +## 常见问题 + +### Q: 配置修改后如何生效? + +A: 需要重启服务或调用 `config.reload()` 方法。 + +### Q: 环境变量优先级为什么最高? + +A: 为了支持容器化部署和 CI/CD 场景,允许在不修改配置文件的情况下动态调整配置。 + +### Q: 验证失败会阻止服务启动吗? + +A: 不会,验证失败只会输出警告日志,服务仍会继续运行。 + +### Q: 如何查看当前生效的配置? + +A: 可以通过 API 调用 `config.getAll()` 获取合并后的完整配置。 diff --git a/remote/docs/指南/NSSM使用指南.md b/remote/docs/指南/NSSM使用指南.md new file mode 100644 index 0000000..5347080 --- /dev/null +++ b/remote/docs/指南/NSSM使用指南.md @@ -0,0 +1,389 @@ +# NSSM 使用指南 + +> NSSM (Non-Sucking Service Manager) 是一个将任意应用程序安装为 Windows 服务的工具。 + +--- + +## 一、NSSM 简介 + +NSSM 可以将 Node.js 应用、Python 脚本、批处理文件等任何可执行程序安装为 Windows 服务,实现开机自启动和自动重启。 + +### 本地文件位置 + +``` +nssm/ +└── nssm.exe # 64 位版本 +``` + +--- + +## 二、安装服务 + +### 基本语法 + +```powershell +nssm install <服务名> <程序路径> [参数] +``` + +### 安装 Remote 应用服务 + +```powershell +# 以管理员身份运行 PowerShell +cd C:\Users\xuanchi\Desktop\remote\nssm + +# 安装服务 +.\nssm.exe install RemoteApp "C:\Program Files\nodejs\node.exe" "C:\Users\xuanchi\Desktop\remote\index.js" +``` + +### 使用图形界面安装 + +```powershell +# 打开图形界面 +.\nssm.exe install RemoteApp +``` + +在弹出的窗口中配置: + +| 标签页 | 配置项 | 值 | +|--------|--------|-----| +| Application | Path | `C:\Program Files\nodejs\node.exe` | +| Application | Startup directory | `C:\Users\xuanchi\Desktop\remote` | +| Application | Arguments | `index.js` | +| I/O | Output (stdout) | `C:\Users\xuanchi\Desktop\remote\logs\service.log` | +| I/O | Error (stderr) | `C:\Users\xuanchi\Desktop\remote\logs\error.log` | + +--- + +## 三、管理服务 + +### 启动服务 + +```powershell +# 通过 NSSM +.\nssm.exe start RemoteApp + +# 或通过 Windows 命令 +net start RemoteApp + +# 或通过 sc 命令 +sc start RemoteApp +``` + +### 停止服务 + +```powershell +# 通过 NSSM +.\nssm.exe stop RemoteApp + +# 或通过 Windows 命令 +net stop RemoteApp + +# 或通过 sc 命令 +sc stop RemoteApp +``` + +### 重启服务 + +```powershell +.\nssm.exe restart RemoteApp +``` + +### 查看服务状态 + +```powershell +.\nssm.exe status RemoteApp +``` + +### 编辑服务配置 + +```powershell +# 打开图形界面编辑 +.\nssm.exe edit RemoteApp +``` + +### 删除服务 + +```powershell +# 先停止服务 +.\nssm.exe stop RemoteApp + +# 删除服务 +.\nssm.exe remove RemoteApp + +# 确认删除(不弹窗) +.\nssm.exe remove RemoteApp confirm +``` + +--- + +## 四、完整安装示例 + +### 一键安装脚本 + +创建 `install-service.ps1`: + +```powershell +# 以管理员身份运行 +$nssmPath = "C:\Users\xuanchi\Desktop\remote\nssm\nssm.exe" +$nodePath = "C:\Program Files\nodejs\node.exe" +$appPath = "C:\Users\xuanchi\Desktop\remote" +$logPath = "C:\Users\xuanchi\Desktop\remote\logs" + +# 创建日志目录 +if (-not (Test-Path $logPath)) { + New-Item -ItemType Directory -Path $logPath +} + +# 安装服务 +& $nssmPath install RemoteApp $nodePath "index.js" + +# 设置工作目录 +& $nssmPath set RemoteApp AppDirectory $appPath + +# 设置显示名称 +& $nssmPath set RemoteApp DisplayName "Remote Desktop Application" + +# 设置描述 +& $nssmPath set RemoteApp Description "Remote screen streaming service" + +# 设置启动类型(自动) +& $nssmPath set RemoteApp Start SERVICE_AUTO_START + +# 设置日志输出 +& $nssmPath set RemoteApp AppStdout "$logPath\service.log" +& $nssmPath set RemoteApp AppStderr "$logPath\error.log" + +# 设置日志轮转 +& $nssmPath set RemoteApp AppRotateFiles 1 +& $nssmPath set RemoteApp AppRotateBytes 1048576 + +# 设置失败重启 +& $nssmPath set RemoteApp AppExit Default Restart +& $nssmPath set RemoteApp AppRestartDelay 5000 + +# 启动服务 +& $nssmPath start RemoteApp + +Write-Host "RemoteApp service installed and started successfully!" +``` + +### 运行安装脚本 + +```powershell +# 以管理员身份运行 +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\install-service.ps1 +``` + +--- + +## 五、高级配置 + +### 设置环境变量 + +```powershell +.\nssm.exe set RemoteApp AppEnvironmentExtra "NODE_ENV=production" "PORT=3000" +``` + +### 设置服务依赖 + +```powershell +# 依赖其他服务(如需要网络) +.\nssm.exe set RemoteApp DependOnService Tcpip +``` + +### 设置用户账户 + +```powershell +# 使用特定用户运行 +.\nssm.exe set RemoteApp ObjectName ".\username" "password" +``` + +### 设置优先级 + +```powershell +# 设置进程优先级 +.\nssm.exe set RemoteApp AppPriority NORMAL_PRIORITY_CLASS + +# 可选值: +# IDLE_PRIORITY_CLASS +# BELOW_NORMAL_PRIORITY_CLASS +# NORMAL_PRIORITY_CLASS +# ABOVE_NORMAL_PRIORITY_CLASS +# HIGH_PRIORITY_CLASS +# REALTIME_PRIORITY_CLASS +``` + +### 设置失败操作 + +```powershell +# 第一次失败:重启服务 +.\nssm.exe set RemoteApp AppExit Default Restart + +# 重启延迟(毫秒) +.\nssm.exe set RemoteApp AppRestartDelay 5000 + +# 设置 Windows 服务恢复选项 +.\nssm.exe set RemoteApp AppThrottle 1500 +``` + +--- + +## 六、同时安装 FRP 和 Remote 服务 + +### 完整安装脚本 + +创建 `install-all-services.ps1`: + +```powershell +# 以管理员身份运行 +$nssmPath = "C:\Users\xuanchi\Desktop\remote\nssm\nssm.exe" +$nodePath = "C:\Program Files\nodejs\node.exe" +$appPath = "C:\Users\xuanchi\Desktop\remote" +$frpPath = "C:\Users\xuanchi\Desktop\remote\frp" +$logPath = "C:\Users\xuanchi\Desktop\remote\logs" + +# 创建日志目录 +if (-not (Test-Path $logPath)) { + New-Item -ItemType Directory -Path $logPath +} + +# ==================== 安装 FRP 客户端服务 ==================== + +Write-Host "Installing FRP Client service..." + +& $nssmPath install FRPClient "$frpPath\frpc.exe" "-c frpc.toml" +& $nssmPath set FRPClient AppDirectory $frpPath +& $nssmPath set FRPClient DisplayName "FRP Client" +& $nssmPath set FRPClient Description "FRP Client for Remote Access" +& $nssmPath set FRPClient Start SERVICE_AUTO_START +& $nssmPath set FRPClient AppStdout "$logPath\frpc-service.log" +& $nssmPath set FRPClient AppStderr "$logPath\frpc-error.log" +& $nssmPath set FRPClient AppRotateFiles 1 +& $nssmPath set FRPClient AppRotateBytes 1048576 +& $nssmPath set FRPClient AppExit Default Restart +& $nssmPath set FRPClient AppRestartDelay 5000 + +# ==================== 安装 Remote 应用服务 ==================== + +Write-Host "Installing Remote App service..." + +& $nssmPath install RemoteApp $nodePath "index.js" +& $nssmPath set RemoteApp AppDirectory $appPath +& $nssmPath set RemoteApp DisplayName "Remote Desktop Application" +& $nssmPath set RemoteApp Description "Remote screen streaming service" +& $nssmPath set RemoteApp Start SERVICE_AUTO_START +& $nssmPath set RemoteApp AppStdout "$logPath\service.log" +& $nssmPath set RemoteApp AppStderr "$logPath\error.log" +& $nssmPath set RemoteApp AppRotateFiles 1 +& $nssmPath set RemoteApp AppRotateBytes 1048576 +& $nssmPath set RemoteApp AppExit Default Restart +& $nssmPath set RemoteApp AppRestartDelay 5000 + +# 设置 RemoteApp 依赖 FRPClient +& $nssmPath set RemoteApp DependOnService FRPClient + +# ==================== 启动服务 ==================== + +Write-Host "Starting services..." + +& $nssmPath start FRPClient +Start-Sleep -Seconds 3 +& $nssmPath start RemoteApp + +Write-Host "All services installed and started successfully!" +Write-Host "" +Write-Host "Services installed:" +Write-Host " - FRPClient (FRP 客户端)" +Write-Host " - RemoteApp (远程桌面应用)" +Write-Host "" +Write-Host "Access URL: http://146.56.248.142:8080" +``` + +--- + +## 七、卸载服务 + +### 卸载脚本 + +创建 `uninstall-services.ps1`: + +```powershell +# 以管理员身份运行 +$nssmPath = "C:\Users\xuanchi\Desktop\remote\nssm\nssm.exe" + +# 停止并删除 RemoteApp +Write-Host "Removing RemoteApp service..." +& $nssmPath stop RemoteApp +& $nssmPath remove RemoteApp confirm + +# 停止并删除 FRPClient +Write-Host "Removing FRPClient service..." +& $nssmPath stop FRPClient +& $nssmPath remove FRPClient confirm + +Write-Host "All services removed successfully!" +``` + +--- + +## 八、常用命令速查 + +| 操作 | 命令 | +|------|------| +| 安装服务 | `nssm install <服务名> <程序> [参数]` | +| 启动服务 | `nssm start <服务名>` | +| 停止服务 | `nssm stop <服务名>` | +| 重启服务 | `nssm restart <服务名>` | +| 查看状态 | `nssm status <服务名>` | +| 编辑配置 | `nssm edit <服务名>` | +| 删除服务 | `nssm remove <服务名>` | +| 设置参数 | `nssm set <服务名> <参数名> <值>` | +| 获取参数 | `nssm get <服务名> <参数名>` | + +--- + +## 九、常见问题 + +### 1. 服务启动后立即停止 + +**原因:** 程序路径错误或依赖未满足 + +**解决:** +```powershell +# 检查服务日志 +type C:\Users\xuanchi\Desktop\remote\logs\error.log + +# 检查程序路径 +nssm get RemoteApp Application +``` + +### 2. 权限不足 + +**解决:** 以管理员身份运行 PowerShell + +### 3. 端口被占用 + +```powershell +# 查看端口占用 +netstat -ano | findstr :3000 + +# 结束进程 +taskkill /PID /F +``` + +### 4. 服务无法访问网络 + +**解决:** +```powershell +# 设置服务依赖网络 +nssm set RemoteApp DependOnService Tcpip Dhcp Dnscache +``` + +--- + +## 十、参考链接 + +- NSSM 官网: https://nssm.cc/ +- NSSM 下载: https://nssm.cc/download +- NSSM 文档: https://nssm.cc/usage diff --git a/remote/docs/指南/OpenFRP本地客户端配置指南.md b/remote/docs/指南/OpenFRP本地客户端配置指南.md new file mode 100644 index 0000000..c5b7001 --- /dev/null +++ b/remote/docs/指南/OpenFRP本地客户端配置指南.md @@ -0,0 +1,382 @@ +# OpenFRP 本地客户端配置指南 + +> 本指南面向本地 Remote 项目,介绍如何配置和管理 OpenFRP 客户端,实现内网穿透。 + +--- + +## 一、OpenFRP 简介 + +OpenFRP 是一个高性能的反向代理应用,用于将内网服务暴露到公网。 + +### 工作原理 + +``` +┌─────────────┐ ┌─────────────────────┐ ┌─────────────┐ +│ 外部用户 │ ──────▶ │ FRP Server (公网) │ ◀────── │ FRP Client │ +│ │ │ 146.56.248.142 │ 隧道 │ (本机) │ +└─────────────┘ │ :8080 → 转发 │ │ :3000 │ + └─────────────────────┘ └─────────────┘ +``` + +**工作流程:** +1. frpc 启动后主动连接 frps,建立持久隧道 +2. 外部用户访问 `146.56.248.142:8080` +3. frps 将请求通过隧道转发给本地 frpc +4. frpc 将请求转发到本地 `127.0.0.1:3000` +5. 响应按原路返回 + +--- + +## 二、本地项目配置 + +### 当前配置文件 + +配置文件位置:`frp/frpc.toml` + +```toml +serverAddr = "146.56.248.142" +serverPort = 7000 +auth.token = "wzw20040525" + +log.to = "C:\\frp\\frpc.log" +log.level = "info" +log.maxDays = 7 + +[[proxies]] +name = "remote-desktop" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3000 +remotePort = 8080 +``` + +### 配置项说明 + +| 参数 | 说明 | 当前值 | +|------|------|--------| +| serverAddr | FRP 服务端地址 | 146.56.248.142 | +| serverPort | FRP 服务端端口 | 7000 | +| auth.token | 认证令牌 | wzw20040525 | +| log.to | 日志文件路径 | C:\frp\frpc.log | +| log.level | 日志级别 | info | +| log.maxDays | 日志保留天数 | 7 | +| [[proxies]] | 代理配置块 | - | +| name | 代理名称 | remote-desktop | +| type | 代理类型 | tcp | +| localIP | 本地服务 IP | 127.0.0.1 | +| localPort | 本地服务端口 | 3000 | +| remotePort | 远程访问端口 | 8080 | + +--- + +## 三、启动与管理 + +### 启动 FRP 客户端 + +```powershell +# 进入 frp 目录 +cd C:\Users\xuanchi\Desktop\remote\frp + +# 启动客户端 +.\frpc.exe -c frpc.toml +``` + +**成功输出:** +``` +[start] frpc started successfully +``` + +### 启动顺序 + +**必须先启动 FRP 客户端,再启动 Remote 应用** + +1. 启动 FRP 客户端 +2. 启动 Remote 应用:`node index.js` + +### 访问地址 + +| 访问方式 | 地址 | +|---------|------| +| 本地访问 | http://localhost:3000 | +| 外网访问 | http://146.56.248.142:8080 | + +--- + +## 四、多代理配置 + +可以在同一个配置文件中添加多个代理映射: + +```toml +serverAddr = "146.56.248.142" +serverPort = 7000 +auth.token = "wzw20040525" + +log.to = "C:\\frp\\frpc.log" +log.level = "info" +log.maxDays = 7 + +# 远程桌面应用 +[[proxies]] +name = "remote-desktop" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3000 +remotePort = 8080 + +# SSH 服务 +[[proxies]] +name = "ssh" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +remotePort = 2222 + +# Web 服务 +[[proxies]] +name = "web" +type = "tcp" +localIP = "127.0.0.1" +localPort = 80 +remotePort = 8000 + +# MySQL 数据库 +[[proxies]] +name = "mysql" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3306 +remotePort = 3306 +``` + +**注意事项:** +- 每个代理的 `name` 必须唯一 +- `remotePort` 不能重复 +- 添加新代理后需要重启 frpc + +--- + +## 五、代理类型 + +### TCP 代理 + +最常用的类型,适用于大多数服务。 + +```toml +[[proxies]] +name = "tcp-service" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3000 +remotePort = 8080 +``` + +### UDP 代理 + +适用于 DNS、游戏服务器等。 + +```toml +[[proxies]] +name = "udp-service" +type = "udp" +localIP = "127.0.0.1" +localPort = 53 +remotePort = 5353 +``` + +### HTTP 代理 + +适用于 Web 服务,支持域名路由。 + +```toml +[[proxies]] +name = "http-service" +type = "http" +localIP = "127.0.0.1" +localPort = 80 +customDomains = ["www.example.com"] +``` + +### STCP (Secret TCP) + +需要访问密钥,更安全。 + +```toml +# 服务端配置 +[[proxies]] +name = "secret-service" +type = "stcp" +secretKey = "your-secret-key" +localIP = "127.0.0.1" +localPort = 22 + +# 访问端配置 (另一个 frpc) +[[visitors]] +name = "secret-visitor" +type = "stcp" +serverName = "secret-service" +secretKey = "your-secret-key" +bindAddr = "127.0.0.1" +bindPort = 2222 +``` + +--- + +## 六、Windows 服务配置 + +### 使用 NSSM 安装为服务 + +1. 下载 NSSM: https://nssm.cc/download + +2. 安装服务: +```powershell +nssm install FRPClient "C:\Users\xuanchi\Desktop\remote\frp\frpc.exe" "-c" "C:\Users\xuanchi\Desktop\remote\frp\frpc.toml" +nssm start FRPClient +``` + +3. 管理服务: +```powershell +nssm stop FRPClient +nssm restart FRPClient +nssm remove FRPClient +``` + +### 使用 sc 命令 + +```powershell +sc create FRPClient binPath= "C:\Users\xuanchi\Desktop\remote\frp\frpc.exe -c C:\Users\xuanchi\Desktop\remote\frp\frpc.toml" start= auto +sc start FRPClient +sc stop FRPClient +sc delete FRPClient +``` + +### 使用任务计划程序 + +1. 搜索"任务计划程序" +2. 创建基本任务 +3. 触发器:开机时 +4. 操作:启动程序 +5. 程序:`C:\Users\xuanchi\Desktop\remote\frp\frpc.exe` +6. 参数:`-c frpc.toml` +7. 起始位置:`C:\Users\xuanchi\Desktop\remote\frp` + +--- + +## 七、常见问题排查 + +### 1. 连接失败 + +**错误信息:** +``` +login to server failed: dial tcp xxx.xxx.xxx.xxx:7000: connectex: A connection attempt failed +``` + +**排查步骤:** +1. 检查服务端是否运行 +2. 检查防火墙是否开放端口 +3. 检查 auth.token 是否一致 +4. 测试端口连通性: + ```powershell + Test-NetConnection -ComputerName 146.56.248.142 -Port 7000 + ``` + +### 2. 端口被占用 + +```powershell +# 查看端口占用 +netstat -ano | findstr :8080 + +# 结束进程 +taskkill /PID /F +``` + +### 3. 查看日志 + +```powershell +# 查看日志文件 +type C:\frp\frpc.log + +# 实时查看日志 +Get-Content C:\frp\frpc.log -Wait +``` + +### 4. 浏览器显示 ERR_UNSAFE_PORT + +**原因:** Chrome/Edge 阻止了某些端口(如 6000、4045 等) + +**解决方法:** 修改 `remotePort` 为安全端口(8080、8000、5000、8888、3001) + +### 5. 连接成功但无画面 + +**排查步骤:** +1. 检查本地访问 `http://localhost:3000` 是否正常 +2. 检查 Remote 应用是否启动 +3. 检查 FRP 日志是否有错误 + +--- + +## 八、常用命令速查 + +```powershell +# 启动 FRP 客户端 +.\frpc.exe -c frpc.toml + +# 查看进程 +tasklist | findstr frpc + +# 停止 FRP +taskkill /F /IM frpc.exe + +# 重启 FRP +taskkill /F /IM frpc.exe; .\frpc.exe -c frpc.toml + +# 测试端口连通性 +Test-NetConnection -ComputerName 146.56.248.142 -Port 7000 + +# 查看日志 +type C:\frp\frpc.log +``` + +--- + +## 九、安全建议 + +1. **使用强密码令牌** + ```toml + auth.token = "complex_random_string_here" + ``` + +2. **使用 STCP 替代 TCP** + - 需要密钥才能访问,更安全 + +3. **启用 TLS 加密** + ```toml + transport.tls.enable = true + ``` + +4. **定期更换 Token** + - 建议每 3-6 个月更换一次 + +--- + +## 十、常用端口参考 + +| 服务 | 默认端口 | 说明 | +|------|---------|------| +| SSH | 22 | 远程登录 | +| HTTP | 80 | Web 服务 | +| HTTPS | 443 | 安全 Web 服务 | +| 远程桌面 (RDP) | 3389 | Windows 远程桌面 | +| MySQL | 3306 | 数据库 | +| Redis | 6379 | 缓存服务 | +| PostgreSQL | 5432 | 数据库 | +| MongoDB | 27017 | 数据库 | +| VNC | 5900 | 远程桌面 | + +--- + +## 十一、参考链接 + +- FRP 官方文档: https://gofrp.org/zh-cn/docs/ +- FRP GitHub: https://github.com/fatedier/frp +- FRP 下载地址: https://github.com/fatedier/frp/releases +- 腾讯云 OpenFRP 配置教程: [腾讯云 OpenFRP 配置教程.md](./腾讯云%20OpenFRP%20配置教程.md) diff --git a/remote/docs/指南/腾讯云 BBR 优化指南.md b/remote/docs/指南/腾讯云 BBR 优化指南.md new file mode 100644 index 0000000..f6cfae8 --- /dev/null +++ b/remote/docs/指南/腾讯云 BBR 优化指南.md @@ -0,0 +1,320 @@ +# 腾讯云服务器 BBR 优化指南 + +> 本指南详细讲解如何在腾讯云轻量服务器上开启 BBR 拥塞控制算法,优化 FRP 内网穿透性能。 +> +> **适用场景**:远程桌面、视频流传输、高延迟网络环境 +> +> **预计耗时**:5 分钟 + +--- + +## 📋 目录 + +- [一、BBR 简介](#一bbr-简介) +- [二、为什么远程桌面需要 BBR](#二为什么远程桌面需要-bbr) +- [三、开启 BBR](#三开启-bbr) +- [四、验证配置](#四验证配置) +- [五、效果分析](#五效果分析) +- [六、常见问题](#六常见问题) + +--- + +## 一、BBR 简介 + +### 1.1 什么是 BBR? + +BBR(Bottleneck Bandwidth and RTT)是 Google 开发的 TCP 拥塞控制算法,通过实时测量网络带宽和往返延迟来优化数据传输。 + +### 1.2 BBR vs 传统算法 + +| 对比项 | 传统算法(Cubic/Reno) | BBR | +|--------|----------------------|-----| +| 判断依据 | 丢包 = 网络拥堵 | 实时测量带宽和延迟 | +| 遇到丢包 | 大幅降速 | 保持稳定 | +| 高延迟网络 | 性能下降明显 | 保持稳定 | +| 网络波动 | 频繁调整 | 平滑过渡 | + +### 1.3 BBR 的核心作用 + +**一句话总结**:BBR 让 TCP 连接在"烂网络"下表现更好,而不是让"好网络"变得更快。 + +**BBR 不能**: +- ❌ 突破物理带宽上限 +- ❌ 让好网络变得更快 +- ❌ 增加运营商给的上行/下行速度 + +**BBR 能**: +- ✅ 高延迟网络下保持稳定传输 +- ✅ 有丢包环境下减少性能损失 +- ✅ 网络波动时平滑过渡 + +--- + +## 二、为什么远程桌面需要 BBR + +### 2.1 远程桌面的网络特性 + +远程桌面应用具有以下特点: +- 持续的视频流传输 +- 对延迟敏感 +- 对丢包敏感(视频卡顿) +- 需要稳定的带宽 + +### 2.2 FRP 内网穿透架构 + +``` +本地电脑 → FRP客户端 → 腾讯云服务器 → 外网用户 + TCP长连接 TCP转发 +``` + +BBR 在服务端开启后: +- 优化 FRP 服务端 → 外网用户的 TCP 连接 +- 优化 FRP 客户端 → 服务端的 TCP 连接(双向收益) + +### 2.3 适用场景分析 + +| 场景 | BBR 效果 | +|------|----------| +| 本地网络质量好(低延迟、无丢包) | 几乎无差异 | +| 跨国/跨省访问(高延迟) | 明显提升 | +| WiFi 信号不稳定(有丢包) | 明显更流畅 | +| 高峰期网络拥堵 | 更稳定 | +| 移动网络访问(4G/5G) | 更稳定 | + +--- + +## 三、开启 BBR + +### 3.1 检查当前配置 + +SSH 连接到腾讯云服务器: + +```bash +# 检查当前拥塞控制算法 +sysctl net.ipv4.tcp_congestion_control +# 默认输出:net.ipv4.tcp_congestion_control = cubic + +# 检查内核版本(Ubuntu 24.04 默认支持 BBR) +uname -r +# 输出:6.8.x-generic +``` + +### 3.2 开启 BBR + +```bash +# 添加 BBR 配置 +echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf +echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf + +# 应用配置 +sudo sysctl -p +``` + +### 3.3 添加网络优化参数(推荐) + +编辑配置文件: + +```bash +sudo nano /etc/sysctl.conf +``` + +添加以下内容: + +```ini +# BBR 基础配置 +net.core.default_qdisc = fq +net.ipv4.tcp_congestion_control = bbr + +# BBR 配合的网络优化 +net.core.rmem_max = 16777216 +net.core.wmem_max = 16777216 +net.ipv4.tcp_rmem = 4096 87380 16777216 +net.ipv4.tcp_wmem = 4096 87380 16777216 +net.core.netdev_max_backlog = 5000 +net.ipv4.tcp_fastopen = 3 +net.ipv4.tcp_slow_start_after_idle = 0 +``` + +应用配置: + +```bash +sudo sysctl -p +``` + +### 3.4 重启 FRP 服务 + +让新的网络配置对 FRP 连接生效: + +```bash +sudo systemctl restart frps +sudo systemctl status frps +``` + +--- + +## 四、验证配置 + +### 4.1 验证 BBR 是否生效 + +```bash +# 检查拥塞控制算法 +sysctl net.ipv4.tcp_congestion_control +# 输出:net.ipv4.tcp_congestion_control = bbr + +# 检查内核模块 +lsmod | grep bbr +# 输出:tcp_bbr 20480 X +``` + +### 4.2 验证 TCP 连接使用 BBR + +```bash +# 查看 FRP 的 TCP 连接是否使用 BBR +ss -tin | grep -A 1 :7000 +``` + +期望输出(看到 `bbr` 字样): + +``` +ESTAB 0 0 146.56.248.142:7000 xxx.xxx.xxx.xxx:xxxx + bbr cwnd:10 ... +``` + +### 4.3 配置验证清单 + +| 检查项 | 命令 | 期望结果 | +|--------|------|----------| +| BBR 算法生效 | `sysctl net.ipv4.tcp_congestion_control` | `= bbr` | +| 队列调度器 | `sysctl net.core.default_qdisc` | `= fq` | +| 内核模块加载 | `lsmod \| grep bbr` | `tcp_bbr ...` | +| FRP 服务运行 | `sudo systemctl status frps` | `active (running)` | + +--- + +## 五、效果分析 + +### 5.1 为什么带宽没有变化? + +这是**正常现象**,原因如下: + +**1. BBR 不增加带宽上限** + +BBR 是优化 TCP 拥塞控制的算法,它不能突破物理带宽限制: + +| 误解 | 实际 | +|------|------| +| BBR 会增加带宽 | ❌ 带宽上限由运营商决定 | +| BBR 会提升速度 | ❌ 只是在弱网环境下更稳定 | + +**2. 瓶颈可能在本地** + +``` +完整链路: +本地电脑(上行) → FRP客户端 → 腾讯云服务器(200Mbps) → 外网用户 + ↑ + 瓶颈在这里 +``` + +家庭宽带典型上行: +- 100M 宽带:上行约 20-30 Mbps +- 200M 宽带:上行约 30-50 Mbps +- 有些地区上行只有 10 Mbps + +### 5.2 BBR 真正有用的场景 + +| 场景 | 传统算法 | BBR | 效果 | +|------|----------|-----|------| +| 网络好(低延迟、无丢包) | 满速 | 满速 | 无差异 | +| 高延迟(跨省/跨国) | 下降 30-50% | 保持稳定 | 明显提升 | +| 有丢包(WiFi/移动网络) | 大幅下降 | 基本不受影响 | 明显提升 | +| 网络波动 | 频繁卡顿 | 平滑过渡 | 更稳定 | + +### 5.3 如何正确测试 BBR 效果 + +**方法 1:实际场景测试** + +- 找一个**远距离**的朋友访问(跨省) +- 在**手机 4G/5G 网络**下访问 +- 在**网络高峰期**(晚上 8-10 点)测试 + +**方法 2:使用 iperf3 测试** + +服务器端: + +```bash +sudo apt install iperf3 -y +iperf3 -s +``` + +客户端: + +```powershell +iperf3 -c 146.56.248.142 -t 30 +``` + +--- + +## 六、常见问题 + +### 6.1 BBR 开启后带宽没变化? + +这是正常的,BBR 不增加带宽上限。BBR 的作用是在弱网环境下保持稳定,而不是提升带宽。 + +### 6.2 如何回滚 BBR? + +如果需要恢复到默认算法: + +```bash +sudo sed -i '/net.ipv4.tcp_congestion_control/d' /etc/sysctl.conf +sudo sed -i '/net.core.default_qdisc/d' /etc/sysctl.conf +echo "net.ipv4.tcp_congestion_control=cubic" | sudo tee -a /etc/sysctl.conf +echo "net.core.default_qdisc=fq_codel" | sudo tee -a /etc/sysctl.conf +sudo sysctl -p +``` + +### 6.3 BBR v1 和 BBR v2 的区别? + +Ubuntu 24.04 默认是 BBR v1,已经足够优秀。BBR v2/v3 需要手动编译内核模块,不推荐新手操作。 + +### 6.4 腾讯云是否支持 BBR? + +腾讯云轻量服务器默认没有限制 BBR,可以直接开启。 + +### 6.5 需要在本地电脑也开启 BBR 吗? + +不需要。服务端开启 BBR 已经可以优化 TCP 连接的双向性能。 + +--- + +## 附录:配置参数说明 + +| 参数 | 说明 | 推荐值 | +|------|------|--------| +| `net.core.default_qdisc` | 默认队列调度算法 | fq | +| `net.ipv4.tcp_congestion_control` | TCP 拥塞控制算法 | bbr | +| `net.core.rmem_max` | TCP 读缓冲区最大值 | 16777216 (16MB) | +| `net.core.wmem_max` | TCP 写缓冲区最大值 | 16777216 (16MB) | +| `net.ipv4.tcp_rmem` | TCP 读缓冲区动态调整 | 4096 87380 16777216 | +| `net.ipv4.tcp_wmem` | TCP 写缓冲区动态调整 | 4096 87380 16777216 | +| `net.core.netdev_max_backlog` | 网卡队列长度 | 5000 | +| `net.ipv4.tcp_fastopen` | TCP Fast Open | 3 (客户端+服务端) | +| `net.ipv4.tcp_slow_start_after_idle` | 空闲后慢启动 | 0 (禁用) | + +--- + +## 总结 + +| 问题 | 答案 | +|------|------| +| BBR 能提升带宽上限吗? | ❌ 不能 | +| BBR 能让好网络更快吗? | ❌ 几乎不能 | +| BBR 能让弱网更稳定吗? | ✅ 能,效果明显 | +| 远程桌面场景需要 BBR 吗? | ✅ 需要(外网访问场景多) | + +**建议**:保持 BBR 开启,没有坏处,在跨省访问、移动网络、网络高峰期时会有更好的体验。 + +--- + +*文档版本:v1.0* +*更新时间:2026-03-03* +*适用系统:Ubuntu 24.04 LTS* diff --git a/remote/docs/指南/腾讯云 OpenFRP 配置教程.md b/remote/docs/指南/腾讯云 OpenFRP 配置教程.md new file mode 100644 index 0000000..209a094 --- /dev/null +++ b/remote/docs/指南/腾讯云 OpenFRP 配置教程.md @@ -0,0 +1,977 @@ +# 腾讯云 + OpenFRP 内网穿透完整配置教程 + +> 本教程详细讲解如何使用腾讯云轻量服务器 + OpenFRP 实现内网穿透,让你的远程桌面应用可以从外网访问。 +> +> **适用场景**:远程办公、远程桌面、内网服务暴露等 +> +> **预计耗时**:30-60 分钟 +> +> **成本**:约 45 元/月(腾讯云服务器费用) + +--- + +## 📋 目录 + +- [一、准备工作](#一准备工作) +- [二、购买并配置腾讯云服务器](#二购买并配置腾讯云服务器) +- [三、配置 FRP 服务端](#三配置 frp 服务端) +- [四、配置本地 FRP 客户端](#四配置本地 frp 客户端) +- [五、启动和测试](#五启动和测试) +- [六、常见问题排查](#六常见问题排查) +- [七、优化建议](#七优化建议) + +--- + +## 一、准备工作 + +### 1.1 所需材料 + +| 项目 | 说明 | +|------|------| +| 腾讯云账号 | 需要实名认证 | +| 本地电脑 | Windows 系统 | +| Remote 应用 | 已配置好的远程桌面应用 | +| 预算 | 约 45 元/月 | + +### 1.2 技术栈说明 + +| 组件 | 版本 | 作用 | +|------|------|------| +| 腾讯云轻量服务器 | 2 核 2GB 200Mbps | FRP 服务端 + 流量中转 | +| Ubuntu | 24.04 LTS | 服务器操作系统 | +| FRP | v0.61.1 | 内网穿透工具 | +| Node.js | v16+ | 运行 Remote 应用 | + +### 1.3 端口规划 + +| 端口 | 用途 | 说明 | +|------|------|------| +| 7000 | FRP 服务端监听端口 | 客户端连接服务端 | +| 7500 | FRP 管理面板 | 查看连接状态 | +| 8080 | 远程桌面访问端口 | 浏览器访问此端口 | +| 3000 | Remote 应用本地端口 | 本地服务运行端口 | + +--- + +## 二、购买并配置腾讯云服务器 + +### 2.1 购买服务器 + +#### 步骤 1:进入腾讯云官网 + +1. 访问 https://cloud.tencent.com/ +2. 登录账号(需要完成实名认证) + +#### 步骤 2:选择轻量应用服务器 + +1. 导航栏选择 **产品** → **云服务器** → **轻量应用服务器** +2. 点击 **立即购买** + +#### 步骤 3:选择配置 + +**套餐选择**: +``` +锐驰型套餐 +- 地域:上海/北京/广州/成都/南京(选择离你最近的) +- CPU&内存:2 核 2GB +- 系统盘:40GB SSD +- 峰值带宽:200Mbps +- 流量包:无限流量 +- 价格:45 元/月 或 459 元/年(85 折) +``` + +**镜像选择**: +``` +操作系统:Ubuntu 24.04 LTS 64 位 ✅ +``` + +> ⚠️ **重要**:不要选择 Windows Server,需要额外授权费且资源占用高 + +#### 步骤 4:配置登录方式 + +**推荐选择**:自定义密码 +``` +用户名:ubuntu +密码:设置一个强密码(建议 12 位以上,包含大小写字母 + 数字 + 特殊符号) +``` + +#### 步骤 5:确认购买 + +1. 选择购买时长(建议直接买 1 年,享受 85 折) +2. 勾选服务协议 +3. 点击"立即购买"并完成支付 + +--- + +### 2.2 获取服务器信息 + +购买完成后,在腾讯云控制台记录以下信息: + +``` +公网 IP 地址:xxx.xxx.xxx.xxx +用户名:ubuntu +密码:你设置的密码 +``` + +--- + +### 2.3 SSH 连接服务器 + +#### Windows 用户(PowerShell) + +```powershell +# 打开 PowerShell +ssh ubuntu@你的服务器公网 IP + +# 首次连接会提示确认指纹,输入 yes +# 然后输入密码(输入时不显示,正常输入即可) +``` + +#### 成功登录后看到: + +``` +Welcome to Ubuntu 24.04 LTS (GNU/Linux ...) +ubuntu@VM-4-13-ubuntu:~$ +``` + +--- + +### 2.4 配置服务器防火墙(安全组) + +#### 步骤 1:进入安全组配置 + +1. 登录腾讯云控制台 +2. 进入 **轻量应用服务器** 页面 +3. 找到你的服务器实例,点击进入详情页 +4. 选择 **防火墙** 或 **安全组** 标签页 +5. 点击 **添加规则** 或 **配置规则** + +#### 步骤 2:添加入站规则 + +添加以下 3 条规则: + +| 应用类型 | 来源 | 协议 | 端口 | 策略 | 备注 | +|---------|------|------|------|------|------| +| 自定义 | 全部 IPv4 地址 | TCP | 7000 | 允许 | FRP 客户端连接 | +| 自定义 | 全部 IPv4 地址 | TCP | 7500 | 允许 | FRP 管理面板 | +| 自定义 | 全部 IPv4 地址 | TCP | 8080 | 允许 | Remote 桌面访问 | + +**添加步骤**: +1. 点击"添加规则" +2. 应用类型选择"自定义" +3. 来源填写"全部 IPv4 地址"或"0.0.0.0/0" +4. 协议选择"TCP" +5. 端口填写"7000" +6. 策略选择"允许" +7. 备注填写"FRP 客户端连接" +8. 点击确定 + +**重复以上步骤添加 7500 和 8080 端口** + +--- + +## 三、配置 FRP 服务端 + +### 3.1 下载 FRP + +```bash +# SSH 连接到服务器后,执行以下命令 + +# 1. 切换到 root 用户(获取管理员权限) +sudo -i + +# 2. 创建 FRP 目录 +mkdir -p /opt/frp +cd /opt/frp + +# 3. 下载 FRP(v0.61.1 版本) +wget https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz + +# 4. 解压 +tar -xzvf frp_0.61.1_linux_amd64.tar.gz + +# 5. 进入目录 +cd frp_0.61.1_linux_amd64 + +# 6. 查看文件 +ls -la +``` + +**应该看到**: +``` +frpc frpc.toml +frps frps.toml +LICENSE +``` + +--- + +### 3.2 生成安全 Token + +Token 是服务端和客户端之间的认证密码,必须保密。 + +```bash +# 生成随机 token +openssl rand -base64 32 +``` + +**示例输出**(保存这个值): +``` +Kj8Hn2bVx9Qm5Lp3Wr7Yt4Za6Uc1Fd0Sg8Ik2Ow5Ev9Mx3Rq7Nj6Hb4Lp0Za2Uc +``` + +> ⚠️ **重要**:将生成的 token 记录下来,后面配置要用! + +--- + +### 3.3 配置 FRP 服务端 + +```bash +# 编辑配置文件 +nano frps.ini +``` + +**粘贴以下配置**(修改 token 为你生成的值): + +```ini +[common] +bind_port = 7000 +bind_addr = 0.0.0.0 +token = 你的 token(上面 openssl 生成的值) +dashboard_port = 7500 +dashboard_user = admin +dashboard_pwd = Admin@2024Secure +log_file = /opt/frp/frps.log +log_level = info +log_max_days = 7 +``` + +**配置说明**: + +| 参数 | 说明 | 推荐值 | +|------|------|--------| +| bind_port | FRP 服务端监听端口 | 7000 | +| bind_addr | 监听地址(0.0.0.0 表示所有网卡) | 0.0.0.0 | +| token | 认证密码(服务端客户端必须一致) | 随机生成的字符串 | +| dashboard_port | 管理面板端口 | 7500 | +| dashboard_user | 管理面板用户名 | admin | +| dashboard_pwd | 管理面板密码 | 复杂密码 | +| log_file | 日志文件路径 | /opt/frp/frps.log | +| log_level | 日志级别 | info | +| log_max_days | 日志保留天数 | 7 | + +**保存退出**: +1. 按 `Ctrl + O` +2. 按 `Enter` 确认 +3. 按 `Ctrl + X` 退出 + +--- + +### 3.4 启动 FRP 服务端 + +```bash +# 后台启动 FRP +nohup ./frps -c frps.ini > /dev/null 2>&1 & + +# 查看进程(确认启动成功) +ps aux | grep frps + +# 查看端口监听 +netstat -tlnp | grep 7000 +``` + +**成功输出示例**: +``` +root 12345 0.0 0.1 12345 6789 ? Sl 20:XX 0:00 ./frps -c frps.ini +tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN 12345/frps +tcp 0 0 0.0.0.0:7500 0.0.0.0:* LISTEN 12345/frps +``` + +--- + +### 3.5 配置开机自启动(Systemd 服务) + +```bash +# 创建 systemd 服务文件 +nano /etc/systemd/system/frps.service +``` + +**粘贴以下内容**: + +```ini +[Unit] +Description=FRP Server +After=network.target + +[Service] +Type=simple +User=ubuntu +Group=ubuntu +WorkingDirectory=/opt/frp/frp_0.61.1_linux_amd64 +ExecStart=/opt/frp/frp_0.61.1_linux_amd64/frps -c /opt/frp/frp_0.61.1_linux_amd64/frps.ini +Restart=on-failure +RestartSec=5s +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +``` + +**保存退出**(Ctrl+O → Enter → Ctrl+X) + +```bash +# 重新加载 systemd 配置 +sudo systemctl daemon-reload + +# 停止手动启动的 FRP +sudo pkill frps + +# 启用并启动服务 +sudo systemctl enable frps +sudo systemctl start frps + +# 查看服务状态 +sudo systemctl status frps +``` + +**成功状态**: +``` +● frps.service - FRP Server + Loaded: loaded (/etc/systemd/system/frps.service; enabled; preset: enabled) + Active: active (running) since Mon 2026-03-02 20:48:25 CST; 8s ago +``` + +按 `Ctrl + C` 退出状态查看 + +--- + +### 3.6 验证服务端配置 + +```bash +# 查看日志 +tail -f /opt/frp/frps.log + +# 查看管理面板 +# 浏览器访问:http://你的服务器IP:7500 +# 用户名:admin,密码:Admin@2024Secure +``` + +--- + +## 四、配置本地 FRP 客户端 + +### 4.1 下载 FRP Windows 客户端 + +#### 方法 1:浏览器下载 + +1. 访问:https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_windows_amd64.zip +2. 下载到本地(如 `Downloads` 文件夹) + +#### 方法 2:PowerShell 下载 + +```powershell +# 创建目录 +mkdir C:\frp +cd C:\frp + +# 下载 +Invoke-WebRequest -Uri "https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_windows_amd64.zip" -OutFile "C:\frp\frp.zip" + +# 解压 +Expand-Archive -Path "C:\frp\frp.zip" -DestinationPath "C:\frp" -Force + +# 进入目录 +cd C:\frp\frp_0.61.1_windows_amd64 + +# 查看文件 +dir +``` + +**应该看到**: +``` +frpc.exe +frps.exe +frpc.toml +frps.toml +LICENSE +``` + +--- + +### 4.2 配置 FRP 客户端 + +#### 步骤 1:创建配置文件 + +在 `C:\frp\frp_0.61.1_windows_amd64` 目录创建 `frpc.ini`: + +```powershell +notepad frpc.ini +``` + +#### 步骤 2:粘贴配置 + +```ini +[common] +server_addr = 你的腾讯云公网 IP +server_port = 7000 +token = 你的 token(和服务端一致) +log_file = C:\frp\frpc.log +log_level = info +log_max_days = 7 + +[remote-desktop] +type = tcp +local_ip = 127.0.0.1 +local_port = 3000 +remote_port = 8080 +``` + +**⚠️ 必须修改的参数**: + +| 参数 | 说明 | 示例 | +|------|------|------| +| server_addr | 腾讯云公网 IP | 146.56.248.142 | +| token | 和服务端一致的 token | Kj8Hn2bVx9... | +| local_port | Remote 应用本地端口 | 3000 | +| remote_port | 服务器访问端口 | 8080 | + +#### 步骤 3:保存 + +保存为 `frpc.ini`(注意编码选择 UTF-8) + +--- + +### 4.3 启动 FRP 客户端 + +```powershell +# 在 C:\frp\frp_0.61.1_windows_amd64 目录 +.\frpc.exe -c frpc.ini +``` + +**成功输出**: +``` +start service success +[remote-desktop] start proxy success +``` + +> ⚠️ **注意**:会看到 `WARNING: ini format is deprecated...` 提示,可以忽略,不影响使用 + +--- + +### 4.4 配置开机自启动(Windows 服务) + +#### 方法 1:创建批处理文件 + +创建 `C:\frp\install-service.bat`: + +```batch +@echo off +sc create FRPClient binPath= "C:\frp\frp_0.61.1_windows_amd64\frpc.exe -c C:\frp\frp_0.61.1_windows_amd64\frpc.ini" start= auto +sc description FRPClient "FRP Client for Remote Access" +sc start FRPClient +echo FRP Client service installed successfully! +pause +``` + +**以管理员身份运行**这个批处理文件 + +#### 方法 2:使用任务计划程序 + +1. 搜索"任务计划程序" +2. 创建基本任务 +3. 触发器:开机时 +4. 操作:启动程序 +5. 程序:`C:\frp\frp_0.61.1_windows_amd64\frpc.exe` +6. 参数:`-c C:\frp\frp_0.61.1_windows_amd64\frpc.ini` + +--- + +## 五、启动和测试 + +### 5.1 启动顺序 + +**必须先启动 FRP 客户端,再启动 Remote 应用** + +#### 步骤 1:启动 FRP 客户端 + +```powershell +cd C:\frp\frp_0.61.1_windows_amd64 +.\frpc.exe -c frpc.ini +``` + +保持这个窗口运行(不要关闭) + +#### 步骤 2:启动 Remote 应用 + +**打开新的 PowerShell 窗口**: + +```powershell +cd C:\Users\xuanchi\Desktop\remote +node .\index.js +``` + +**成功输出**: +``` +Remote screen streaming server started +Encoder: mpeg1video +WebSocket: ws://localhost:3000/ws +HTTP: http://localhost:3000 +``` + +--- + +### 5.2 测试访问 + +#### 测试 1:本地访问 + +``` +http://localhost:3000 +``` + +应该能看到 Remote 应用界面 + +#### 测试 2:外网访问 + +``` +http://你的腾讯云公网IP:8080 +``` + +应该能看到相同的 Remote 应用界面 + +#### 测试 3:FRP 管理面板 + +``` +http://你的腾讯云公网IP:7500 +``` + +**登录信息**: +- 用户名:`admin` +- 密码:`Admin@2024Secure` + +**应该看到**: +- 在线代理数量:1 +- [remote-desktop] 状态:在线 + +--- + +### 5.3 流量测试 + +```bash +# 在服务器上查看流量 +watch -n 1 'netstat -an | grep 8080 | wc -l' +``` + +或者在 FRP 管理面板查看实时流量统计 + +--- + +## 六、常见问题排查 + +### 6.1 FRP 客户端无法连接服务端 + +**错误信息**: +``` +login to server failed: dial tcp xxx.xxx.xxx.xxx:7000: connectex: A connection attempt failed +``` + +**排查步骤**: + +1. **检查服务器安全组** + ```bash + # SSH 到服务器 + sudo ufw status + # 或 + sudo iptables -L -n | grep 7000 + ``` + +2. **检查 FRP 服务端状态** + ```bash + sudo systemctl status frps + sudo netstat -tlnp | grep 7000 + ``` + +3. **检查 token 是否一致** + ```bash + # 服务端 + cat /opt/frp/frp_0.61.1_linux_amd64/frps.ini | grep token + + # 客户端 + type C:\frp\frp_0.61.1_windows_amd64\frpc.ini | findstr token + ``` + +4. **测试端口连通性** + ```powershell + # 在本地 PowerShell + Test-NetConnection -ComputerName 你的服务器IP -Port 7000 + ``` + +--- + +### 6.2 浏览器显示 ERR_UNSAFE_PORT + +**错误信息**: +``` +嗯... 无法访问此页面 +ERR_UNSAFE_PORT +``` + +**原因**:Chrome/Edge 阻止了某些端口(如 6000、4045 等) + +**解决方法**: +1. 修改 FRP 客户端配置 `remote_port = 8080`(或其他安全端口) +2. 修改腾讯云安全组规则 +3. 重启 FRP 客户端 + +**安全端口推荐**:8080、8000、5000、8888、3001 + +--- + +### 6.3 Remote 应用无法启动 + +**错误信息**: +``` +Error: Cannot find module 'xxx' +``` + +**解决方法**: +```powershell +cd C:\Users\xuanchi\Desktop\remote +npm install +node .\index.js +``` + +--- + +### 6.4 连接成功但无画面 + +**可能原因**: +1. Remote 应用未启动 +2. 端口映射错误 +3. 编码器问题 + +**排查步骤**: + +1. **检查本地访问** + ``` + http://localhost:3000 + ``` + 如果本地也无法访问,说明 Remote 应用未启动 + +2. **检查 FRP 日志** + ```powershell + type C:\frp\frpc.log + ``` + +3. **检查 Remote 应用日志** + 查看 PowerShell 输出 + +--- + +### 6.5 延迟高/卡顿 + +**优化建议**: + +1. **降低视频码率** + ```javascript + // 修改 FFmpegEncoder.js + this.bitrate = '800k'; // 从 1500k 降低到 800k + ``` + +2. **降低帧率** + ```javascript + this.fps = 20; // 从 30 降低到 20 + ``` + +3. **降低分辨率** + ```javascript + // 修改 scale 参数 + '-vf', 'scale=1024:576:force_original_aspect_ratio=decrease', + ``` + +4. **选择更近的服务器地域** + - 南方用户:广州/上海 + - 北方用户:北京 + - 西南用户:成都 + +--- + +### 6.6 FRP 服务意外停止 + +**检查 systemd 服务状态**: +```bash +sudo systemctl status frps +sudo journalctl -u frps -n 50 +``` + +**重启服务**: +```bash +sudo systemctl restart frps +``` + +**查看日志**: +```bash +tail -f /opt/frp/frps.log +``` + +--- + +## 七、优化建议 + +### 7.1 性能优化 + +#### 服务器端优化 + +```bash +# 1. 更新系统 +sudo apt update && sudo apt upgrade -y + +# 2. 优化网络参数 +sudo nano /etc/sysctl.conf +``` + +**添加以下配置**: +``` +net.core.rmem_max = 16777216 +net.core.wmem_max = 16777216 +net.ipv4.tcp_rmem = 4096 87380 16777216 +net.ipv4.tcp_wmem = 4096 87380 16777216 +``` + +**应用配置**: +```bash +sudo sysctl -p +``` + +#### 客户端优化 + +1. **使用有线网络**(比 WiFi 更稳定) +2. **关闭后台占用带宽的应用** +3. **使用 5GHz WiFi**(如果必须用无线) + +--- + +### 7.2 安全加固 + +#### 修改默认端口 + +```ini +# frps.ini +bind_port = 27000 # 不用默认的 7000 +dashboard_port = 27500 # 不用默认的 7500 +``` + +#### 启用 HTTPS 管理面板 + +```ini +# frps.ini +dashboard_tls_mode = true +dashboard_tls_cert_file = /path/to/cert.pem +dashboard_tls_key_file = /path/to/key.pem +``` + +#### 限制访问 IP + +```ini +# frps.ini +allow_ports = 2000-3000,3001,4000-50000 +``` + +#### 定期更换 Token + +建议每 3-6 个月更换一次 token + +--- + +### 7.3 监控和告警 + +#### 监控 FRP 状态 + +```bash +# 创建监控脚本 +nano /opt/frp/check_frps.sh +``` + +**内容**: +```bash +#!/bin/bash +if ! pgrep -x "frps" > /dev/null; then + echo "FRPS is not running, restarting..." + systemctl restart frps + echo "FRPS restarted at $(date)" >> /opt/frp/frps_restart.log +fi +``` + +**添加定时任务**: +```bash +crontab -e +``` + +**添加**: +``` +*/5 * * * * /bin/bash /opt/frp/check_frps.sh +``` + +每 5 分钟检查一次 FRP 状态 + +--- + +### 7.4 备份配置 + +#### 备份服务端配置 + +```bash +# 创建备份目录 +mkdir -p /opt/frp/backup + +# 备份配置文件 +cp /opt/frp/frp_0.61.1_linux_amd64/frps.ini /opt/frp/backup/frps_$(date +%Y%m%d).ini +cp /etc/systemd/system/frps.service /opt/frp/backup/ +``` + +#### 备份客户端配置 + +```powershell +# PowerShell 备份 +Copy-Item C:\frp\frp_0.61.1_windows_amd64\frpc.ini ` + "C:\frp\backup\frpc_$(Get-Date -Format 'yyyyMMdd').ini" +``` + +--- + +### 7.5 成本优化 + +#### 1. 选择合适地域 + +- 一线城市(北京/上海/广州):价格稍高,网络质量好 +- 二线城市(成都/南京):价格稍低,覆盖特定区域 + +#### 2. 选择合适时长 + +- 按月购买:45 元/月 +- 按年购买:459 元/年(相当于 38.25 元/月,省 15%) + +#### 3. 利用活动 + +- 腾讯云新用户优惠 +- 双 11、618 等促销活动 +- 学生优惠(如果有学生身份) + +--- + +## 附录 A:完整配置清单 + +### 服务端配置 + +```ini +# /opt/frp/frp_0.61.1_linux_amd64/frps.ini +[common] +bind_port = 7000 +bind_addr = 0.0.0.0 +token = 你的 token +dashboard_port = 7500 +dashboard_user = admin +dashboard_pwd = Admin@2024Secure +log_file = /opt/frp/frps.log +log_level = info +log_max_days = 7 +``` + +### 客户端配置 + +```ini +# C:\frp\frp_0.61.1_windows_amd64\frpc.ini +[common] +server_addr = 你的腾讯云 IP +server_port = 7000 +token = 你的 token +log_file = C:\frp\frpc.log +log_level = info +log_max_days = 7 + +[remote-desktop] +type = tcp +local_ip = 127.0.0.1 +local_port = 3000 +remote_port = 8080 +``` + +--- + +## 附录 B:常用命令速查 + +### 服务端命令 + +```bash +# 查看 FRP 状态 +sudo systemctl status frps + +# 重启 FRP +sudo systemctl restart frps + +# 查看日志 +tail -f /opt/frp/frps.log + +# 查看进程 +ps aux | grep frps + +# 查看端口 +netstat -tlnp | grep 7000 + +# 重启服务器 +sudo reboot +``` + +### 客户端命令 + +```powershell +# 启动 FRP 客户端 +.\frpc.exe -c frpc.ini + +# 查看进程 +tasklist | findstr frpc + +# 停止 FRP +taskkill /F /IM frpc.exe + +# 重启 FRP +taskkill /F /IM frpc.exe; .\frpc.exe -c frpc.ini +``` + +--- + +## 附录 C:资源链接 + +- [FRP 官方 GitHub](https://github.com/fatedier/frp) +- [FRP 官方文档](https://gofrp.org/) +- [腾讯云官网](https://cloud.tencent.com/) +- [Ubuntu 官方文档](https://ubuntu.com/server/docs) + +--- + +## 总结 + +恭喜你完成腾讯云 + OpenFRP 的配置!现在你的 Remote 应用已经可以从外网访问了。 + +### 关键要点回顾: + +1. ✅ 选择 Ubuntu 24.04 镜像 +2. ✅ 配置安全组开放 7000、7500、8080 端口 +3. ✅ 服务端和客户端 token 必须一致 +4. ✅ 避免使用 6000 等不安全端口 +5. ✅ 配置开机自启动保证稳定性 + +### 下一步建议: + +1. 测试外网访问稳定性 +2. 根据需要调整视频质量参数 +3. 配置监控和告警 +4. 定期备份配置文件 + +**如有问题,请查看"常见问题排查"章节或查阅 FRP 官方文档!** + +--- + +*文档版本:v1.0* +*更新时间:2026-03-02* +*适用 FRP 版本:v0.61.1* diff --git a/remote/docs/指南/腾讯云域名与SSL证书配置指南.md b/remote/docs/指南/腾讯云域名与SSL证书配置指南.md new file mode 100644 index 0000000..18bb81a --- /dev/null +++ b/remote/docs/指南/腾讯云域名与SSL证书配置指南.md @@ -0,0 +1,638 @@ +# 腾讯云域名与 SSL 证书配置指南 + +> 本指南详细讲解如何在腾讯云服务器上配置域名解析和 SSL 证书,实现 HTTPS 安全访问。 +> +> **适用场景**:远程桌面 HTTPS 访问、WebSocket 安全连接、WebCodecs API 使用 +> +> **预计耗时**:10 分钟 + +--- + +## 📋 目录 + +- [一、前置知识](#一前置知识) +- [二、准备工作](#二准备工作) +- [三、开放安全组端口](#三开放安全组端口) +- [四、域名 DNS 解析配置](#四域名-dns-解析配置) +- [五、安装 Nginx 和 Certbot](#五安装-nginx-和-certbot) +- [六、配置 Nginx 反向代理](#六配置-nginx-反向代理) +- [七、申请 SSL 证书](#七申请-ssl-证书) +- [八、配置子域名(可选)](#八配置子域名可选) +- [九、自签名证书配置](#九自签名证书配置) +- [十、验证配置](#十验证配置) +- [十一、常见问题](#十一常见问题) + +--- + +## 一、前置知识 + +### 1.1 什么是域名? + +域名是互联网上服务器的地址,方便人类记忆,代替难记的 IP 地址。 + +| 概念 | 示例 | 说明 | +|------|------|------| +| 顶级域名 | `.com`、`.cn`、`.org` | 域名的最后一部分 | +| 主域名 | `example.com` | 你购买的域名 | +| 子域名 | `www.example.com`、`api.example.com` | 主域名下的分支 | + +**域名解析**:将域名转换为 IP 地址的过程。通过 DNS(Domain Name System)服务器完成。 + +### 1.2 什么是 HTTP 和 HTTPS? + +| 协议 | 说明 | 特点 | +|------|------|------| +| HTTP | 超文本传输协议 | 明文传输,不安全 | +| HTTPS | HTTP + SSL/TLS | 加密传输,安全 | + +**为什么需要 HTTPS?** +- 数据加密:防止中间人窃听 +- 身份验证:确认服务器身份 +- 数据完整性:防止数据被篡改 +- 浏览器要求:WebCodecs、某些 API 必须使用 HTTPS + +### 1.3 什么是 SSL/TLS 证书? + +SSL/TLS 证书是数字证书,用于验证服务器身份并建立加密连接。 + +| 证书类型 | 说明 | 适用场景 | +|---------|------|---------| +| DV(域名验证) | 只验证域名所有权 | 个人网站、博客 | +| OV(组织验证) | 验证组织身份 | 企业网站 | +| EV(扩展验证) | 严格验证组织身份 | 金融、电商 | + +**证书颁发机构(CA)**:受信任的证书签发机构,如 Let's Encrypt、DigiCert 等。 + +### 1.4 什么是 Nginx? + +Nginx 是高性能的 Web 服务器和反向代理服务器。 + +**本教程中 Nginx 的作用**: +- 反向代理:将外部请求转发到本地 FRP 端口(8080) +- SSL 终止:处理 HTTPS 加密/解密 +- WebSocket 代理:支持远程桌面的 WebSocket 连接 + +``` +用户浏览器 → Nginx(443端口)→ FRP(8080端口)→ 本地电脑 + HTTPS解密 内网穿透 +``` + +### 1.5 什么是 Certbot? + +Certbot 是 Let's Encrypt 官方推荐的证书申请工具。 + +**Certbot 的作用**: +- 自动申请 SSL 证书 +- 自动配置 Nginx +- 自动续期证书(Let's Encrypt 证书有效期 90 天) + +**Let's Encrypt**:免费的证书颁发机构,由非营利组织 ISRG 运营。 + +### 1.6 整体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 腾讯云服务器 │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ Nginx │────▶│ FRP │────▶│ 本地电脑 │ │ +│ │ :443 │ │ :8080 │ │ :3000 │ │ +│ │ HTTPS │ │ 内网穿透 │ │ 远程桌面 │ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +│ ▲ │ +│ │ SSL证书 │ +│ ┌────┴────┐ │ +│ │ Certbot │ │ +│ │ Let's │ │ +│ │ Encrypt │ │ +│ └─────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▲ + │ DNS解析 + ┌─────┴─────┐ + │ 域名 │ + │ example. │ + │ com │ + └───────────┘ +``` + +**请求流程**: +1. 用户访问 `https://example.com` +2. DNS 解析到腾讯云服务器 IP +3. Nginx 接收 HTTPS 请求,解密 +4. Nginx 反向代理到 FRP 的 8080 端口 +5. FRP 转发到本地电脑的 3000 端口 +6. 本地电脑返回远程桌面画面 + +--- + +## 二、准备工作 + +### 2.1 所需材料 + +| 项目 | 说明 | +|------|------| +| 腾讯云服务器 | 已配置好 FRP 服务端 | +| 域名 | 已购买并完成实名认证 | +| FRP 服务端口 | 默认 8080 | + +### 2.2 端口规划 + +| 端口 | 用途 | 说明 | +|------|------|------| +| 80 | HTTP 访问 | Certbot 验证 + HTTP 重定向 | +| 443 | HTTPS 访问 | 安全访问入口 | +| 8080 | FRP 转发端口 | 远程桌面实际端口 | + +--- + +## 三、开放安全组端口 + +> **重要:必须先开放端口才能继续后续配置!** + +### 3.1 登录腾讯云控制台 + +1. 登录 [腾讯云控制台](https://console.cloud.tencent.com/) +2. 进入「云服务器」→ 选择服务器 →「安全组」 + +### 3.2 添加入站规则 + +点击「添加规则」,添加以下两条规则: + +| 协议 | 端口 | 来源 | 策略 | +|------|------|------|------| +| TCP | 80 | 0.0.0.0/0 | 允许 | +| TCP | 443 | 0.0.0.0/0 | 允许 | + +### 3.3 验证端口 + +在服务器上执行: + +```bash +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw status +``` + +--- + +## 三、域名 DNS 解析配置 + +### 3.1 登录腾讯云 DNS 解析 + +1. 登录 [腾讯云控制台](https://console.cloud.tencent.com/) +2. 进入「我的域名」→ 选择域名 →「解析」 +3. 点击「添加记录」 + +### 3.2 添加 A 记录 + +| 参数 | 值 | +|------|-----| +| 主机记录 | `@`(表示主域名本身) | +| 记录类型 | `A` | +| 记录值 | 腾讯云服务器公网 IP | +| TTL | 默认(600) | + +### 3.3 配置示例 + +假设你的域名是 `example.com`,服务器 IP 是 `123.45.67.89`: + +| 主机记录 | 完整域名 | 说明 | +|---------|---------|------| +| `@` | `example.com` | 主域名 | +| `www` | `www.example.com` | www 子域名 | + +### 3.4 验证 DNS 解析 + +在本地电脑或服务器上执行: + +```bash +ping example.com +``` + +如果返回你的服务器 IP,说明解析生效。 + +--- + +## 四、安装 Nginx 和 Certbot + +### 4.1 SSH 连接服务器 + +```bash +ssh ubuntu@你的服务器公网IP +``` + +### 4.2 安装软件 + +```bash +sudo apt update +sudo apt install -y nginx certbot python3-certbot-nginx +``` + +### 4.3 验证安装 + +```bash +nginx -v +# 输出:nginx version: nginx/1.24.0 + +certbot --version +# 输出:certbot 2.x.x +``` + +--- + +## 五、配置 Nginx 反向代理 + +### 5.1 创建站点配置 + +将 `example.com` 替换为你的实际域名: + +```bash +sudo tee /etc/nginx/sites-available/example.com << 'EOF' +server { + listen 80; + server_name example.com www.example.com; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400; + } +} +EOF +``` + +> **说明**:配置文件名使用域名命名,方便管理。将 `example.com` 替换为你自己的域名。 + +### 5.2 启用站点配置 + +```bash +# 创建软链接 +sudo ln -sf /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ + +# 删除默认站点(可选) +sudo rm -f /etc/nginx/sites-enabled/default + +# 测试配置 +sudo nginx -t +``` + +### 5.3 重载 Nginx + +```bash +sudo systemctl reload nginx +sudo systemctl status nginx +``` + +--- + +## 六、申请 SSL 证书 + +### 6.1 使用 Certbot 申请 + +将 `example.com` 替换为你的实际域名: + +```bash +sudo certbot --nginx -d example.com -d www.example.com +``` + +### 6.2 按提示操作 + +``` +Enter email address: your_email@example.com +# 输入邮箱,用于接收证书到期提醒 + +Please read the Terms of Service: (Y)es/(N)o: Y +# 同意服务条款 + +Would you be willing to share your email address: (Y)es/(N)o: N +# 是否分享邮箱,选 N +``` + +### 6.3 选择 HTTPS 重定向 + +``` +Please choose whether or not to redirect HTTP to HTTPS: +1: No redirect - No further changes to the webserver configuration. +2: Redirect - Make all requests redirect to secure HTTPS access. +Select the appropriate number: 2 +# 选择 2,自动将 HTTP 重定向到 HTTPS +``` + +### 6.4 成功提示 + +``` +Successfully received certificate. +Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem +Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem +This certificate expires on 2026-06-01. +``` + +### 6.5 重要提示 + +> **注意**:Let's Encrypt 有频率限制,每小时最多 5 次失败尝试。如果多次失败会被临时封禁 1 小时。 +> +> 如果遇到封禁,请使用「九、自签名证书配置」作为临时方案。 + +--- + +## 七、配置子域名(可选) + +> 如果你需要为不同服务配置不同的子域名,请参考本章节。 + +### 7.1 添加 DNS 解析 + +在腾讯云控制台添加新的 A 记录: + +| 主机记录 | 记录类型 | 记录值 | +|---------|---------|--------| +| `remote` | A | 服务器 IP | +| `api` | A | 服务器 IP | +| `file` | A | 服务器 IP | + +### 7.2 创建子域名 Nginx 配置 + +以 `remote.example.com` 子域名为例: + +```bash +sudo tee /etc/nginx/sites-available/remote.example.com << 'EOF' +server { + listen 80; + server_name remote.example.com; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400; + } +} +EOF + +sudo ln -sf /etc/nginx/sites-available/remote.example.com /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +### 7.3 申请子域名证书 + +```bash +sudo certbot --nginx -d remote.example.com +``` + +### 7.4 子域名用途示例 + +| 子域名 | 用途 | +|--------|------| +| `remote.example.com` | 远程桌面 | +| `api.example.com` | API 服务 | +| `file.example.com` | 文件服务 | + +--- + +## 八、自签名证书配置 + +> 当 Let's Encrypt 封禁或测试环境时使用 + +### 8.1 生成自签名证书 + +将 `example.com` 替换为你的实际域名: + +```bash +sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/ssl/private/example.com.key \ + -out /etc/ssl/certs/example.com.crt \ + -subj "/CN=example.com" +``` + +### 8.2 配置 Nginx 使用自签名证书 + +将 `example.com` 替换为你的实际域名: + +```bash +sudo tee /etc/nginx/sites-available/example.com << 'EOF' +server { + listen 80; + server_name example.com www.example.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name example.com www.example.com; + + ssl_certificate /etc/ssl/certs/example.com.crt; + ssl_certificate_key /etc/ssl/private/example.com.key; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400; + } +} +EOF + +sudo nginx -t && sudo systemctl reload nginx +``` + +### 8.3 浏览器访问 + +访问 `https://example.com`(替换为你的域名),浏览器会提示"不安全": +1. 点击「高级」 +2. 点击「继续访问」即可使用 + +--- + +## 九、验证配置 + +### 9.1 检查端口监听 + +```bash +sudo netstat -tlnp | grep -E '80|443' +``` + +期望输出: + +``` +tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* nginx +tcp LISTEN 0 511 0.0.0.0:443 0.0.0.0:* nginx +``` + +### 9.2 测试 HTTPS 访问 + +在本地浏览器访问: + +``` +https://example.com +``` + +如果看到小锁图标,说明 HTTPS 配置成功。 + +### 9.3 测试 WebSocket + +打开浏览器控制台(F12),检查 WebSocket 连接: + +``` +WebSocket connected +``` + +### 9.4 检查证书自动续期 + +```bash +sudo certbot renew --dry-run +``` + +如果显示 `Congratulations, all simulated renewals succeeded`,说明自动续期配置正常。 + +--- + +## 十一、常见问题 + +### 11.1 证书申请失败:DNS 解析未生效 + +**错误信息**: + +``` +Failed to verify domain: DNS problem: NXDOMAIN looking up A for example.com +``` + +**解决方法**: + +等待 DNS 解析生效(通常需要几分钟),然后重试: + +```bash +sudo certbot --nginx -d example.com +``` + +### 11.2 证书申请失败:频率限制 + +**错误信息**: + +``` +too many failed authorizations (5) for "example.com" in the last 1h0m0s +``` + +**解决方法**: + +Let's Encrypt 封禁 1 小时,使用自签名证书临时替代: + +```bash +# 参考第八章「自签名证书配置」 +``` + +### 11.3 Nginx 502 Bad Gateway + +**原因**:FRP 服务未运行或端口不正确 + +**解决方法**: + +```bash +# 检查 FRP 服务端是否运行 +sudo netstat -tlnp | grep 8080 + +# 如果没有运行,启动 FRP 服务端 +sudo systemctl start frps +``` + +### 11.4 WebSocket 连接失败 + +**原因**:Nginx 未正确配置 WebSocket 代理 + +**解决方法**: + +确保 Nginx 配置包含以下内容: + +```nginx +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; +``` + +### 11.5 证书过期 + +Let's Encrypt 证书有效期为 90 天,Certbot 会自动续期。 + +手动续期: + +```bash +sudo certbot renew +sudo systemctl reload nginx +``` + +### 11.6 如何删除证书 + +```bash +# 查看证书列表 +sudo certbot certificates + +# 删除指定证书 +sudo certbot delete --cert-name example.com +``` + +--- + +## 附录:完整配置文件 + +### Nginx 站点配置(SSL 配置后) + +将 `example.com` 替换为你的实际域名: + +```nginx +server { + listen 80; + server_name example.com www.example.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name example.com www.example.com; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400; + } +} +``` + +--- + +## 总结 + +| 步骤 | 命令/操作 | +|------|----------| +| 1. 开放端口 | 腾讯云安全组开放 80、443(必须先做!) | +| 2. DNS 解析 | 腾讯云控制台添加 A 记录 | +| 3. 安装软件 | `sudo apt install -y nginx certbot python3-certbot-nginx` | +| 4. 配置 Nginx | 创建 `/etc/nginx/sites-available/你的域名` | +| 5. 申请证书 | `sudo certbot --nginx -d 你的域名` | +| 6. 验证 | 浏览器访问 `https://你的域名` | + +**配置完成后**: +- ✅ 支持 HTTPS 安全访问 +- ✅ 支持 WebSocket 安全连接(WSS) +- ✅ 支持 WebCodecs API +- ✅ 证书自动续期 + +--- + +*文档版本:v1.3* +*更新时间:2026-03-03* +*适用系统:Ubuntu 24.04 LTS* diff --git a/remote/frp/LICENSE b/remote/frp/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/remote/frp/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/remote/frp/frpc-runtime.toml b/remote/frp/frpc-runtime.toml new file mode 100644 index 0000000..9d7cf8f --- /dev/null +++ b/remote/frp/frpc-runtime.toml @@ -0,0 +1,23 @@ +serverAddr = "146.56.248.142" +serverPort = 7000 +auth.token = "wzw20040525" + +log.to = "C:\\Users\\xuanchi\\Desktop\\remote\\logs\\frpc.log" +log.level = "info" +log.maxDays = 7 + +[[proxies]] +name = "remote-desktop" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3000 +remotePort = 8080 + +[[proxies]] +name = "gitea-web" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3001 +remotePort = 8081 + + diff --git a/remote/frp/frpc.exe b/remote/frp/frpc.exe new file mode 100644 index 0000000..205597f Binary files /dev/null and b/remote/frp/frpc.exe differ diff --git a/remote/frp/frpc.toml b/remote/frp/frpc.toml new file mode 100644 index 0000000..eb203a7 --- /dev/null +++ b/remote/frp/frpc.toml @@ -0,0 +1,23 @@ +serverAddr = "146.56.248.142" +serverPort = 7000 +auth.token = "wzw20040525" + +log.to = "C:\\frp\\frpc.log" +log.level = "info" +log.maxDays = 7 + +[[proxies]] +name = "remote-desktop" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3000 +remotePort = 8080 + +[[proxies]] +name = "gitea-web" +type = "tcp" +localIP = "127.0.0.1" +localPort = 3001 +remotePort = 8081 + + diff --git a/remote/frp/frps.exe b/remote/frp/frps.exe new file mode 100644 index 0000000..a414b26 Binary files /dev/null and b/remote/frp/frps.exe differ diff --git a/remote/frp/frps.toml b/remote/frp/frps.toml new file mode 100644 index 0000000..28e6d96 --- /dev/null +++ b/remote/frp/frps.toml @@ -0,0 +1 @@ +bindPort = 7000 diff --git a/remote/gitea/custom/conf/app.ini b/remote/gitea/custom/conf/app.ini new file mode 100644 index 0000000..3814077 --- /dev/null +++ b/remote/gitea/custom/conf/app.ini @@ -0,0 +1,77 @@ +APP_NAME = Xuanchi Git +RUN_USER = xuanchi +WORK_PATH = C:\Users\xuanchi\Desktop\remote\gitea +RUN_MODE = prod + +[database] +DB_TYPE = sqlite3 +HOST = 127.0.0.1:5432 +NAME = gitea +USER = gitea +PASSWD = +SCHEMA = +SSL_MODE = disable +PATH = C:\Users\xuanchi\Desktop\remote\gitea\data\gitea.db +LOG_SQL = false + +[repository] +ROOT = C:/Users/xuanchi/Desktop/remote/gitea/data/gitea-repositories + +[server] +SSH_DOMAIN = localhost +DOMAIN = localhost +HTTP_PORT = 3001 +ROOT_URL = http://localhost:3001/ +APP_DATA_PATH = C:\Users\xuanchi\Desktop\remote\gitea\data +DISABLE_SSH = false +SSH_PORT = 22 +LFS_START_SERVER = true +LFS_JWT_SECRET = R7kPD0XqG0zTeGhLu1r8t4h-Y3FfofYW1_6GPhNnVZg +OFFLINE_MODE = true + +[lfs] +PATH = C:/Users/xuanchi/Desktop/remote/gitea/data/lfs + +[mailer] +ENABLED = false + +[service] +REGISTER_EMAIL_CONFIRM = false +ENABLE_NOTIFY_MAIL = false +DISABLE_REGISTRATION = false +ALLOW_ONLY_EXTERNAL_REGISTRATION = false +ENABLE_CAPTCHA = false +REQUIRE_SIGNIN_VIEW = false +DEFAULT_KEEP_EMAIL_PRIVATE = false +DEFAULT_ALLOW_CREATE_ORGANIZATION = true +DEFAULT_ENABLE_TIMETRACKING = true +NO_REPLY_ADDRESS = noreply.localhost + +[openid] +ENABLE_OPENID_SIGNIN = true +ENABLE_OPENID_SIGNUP = true + +[cron.update_checker] +ENABLED = false + +[session] +PROVIDER = file + +[log] +MODE = console +LEVEL = info +ROOT_PATH = C:/Users/xuanchi/Desktop/remote/gitea/log + +[repository.pull-request] +DEFAULT_MERGE_STYLE = merge + +[repository.signing] +DEFAULT_TRUST_MODEL = committer + +[security] +INSTALL_LOCK = true +INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NzI2Mzc5MjN9.PnM7mBbYjj8dz5dHYDwncvicVHOllHb0cWM9lSUdqIM +PASSWORD_HASH_ALGO = pbkdf2 + +[oauth2] +JWT_SECRET = aLiiycJwXwTxX9ZaE2By40OGAxkVcPsOwz-WzJWHfzA diff --git a/remote/gitea/data/avatars/fe31f2679e1f1d6cc7d45743e834ead9 b/remote/gitea/data/avatars/fe31f2679e1f1d6cc7d45743e834ead9 new file mode 100644 index 0000000..386928b Binary files /dev/null and b/remote/gitea/data/avatars/fe31f2679e1f1d6cc7d45743e834ead9 differ diff --git a/remote/gitea/data/gitea.db b/remote/gitea/data/gitea.db new file mode 100644 index 0000000..d320839 Binary files /dev/null and b/remote/gitea/data/gitea.db differ diff --git a/remote/gitea/data/home/.gitconfig b/remote/gitea/data/home/.gitconfig new file mode 100644 index 0000000..41f6dc3 --- /dev/null +++ b/remote/gitea/data/home/.gitconfig @@ -0,0 +1,23 @@ +[diff] + algorithm = histogram +[core] + logallrefupdates = true + quotePath = false + commitGraph = true + longpaths = true +[gc] + reflogexpire = 90 + writeCommitGraph = true +[user] + name = Gitea + email = gitea@fake.local +[receive] + advertisePushOptions = true + procReceiveRefs = refs/for +[fetch] + writeCommitGraph = true +[safe] + directory = * +[uploadpack] + allowfilter = true + allowAnySHA1InWant = true diff --git a/remote/gitea/data/indexers/issues.bleve/index_meta.json b/remote/gitea/data/indexers/issues.bleve/index_meta.json new file mode 100644 index 0000000..5dc3405 --- /dev/null +++ b/remote/gitea/data/indexers/issues.bleve/index_meta.json @@ -0,0 +1 @@ +{"storage":"boltdb","index_type":"scorch"} \ No newline at end of file diff --git a/remote/gitea/data/indexers/issues.bleve/rupture_meta.json b/remote/gitea/data/indexers/issues.bleve/rupture_meta.json new file mode 100644 index 0000000..978d526 --- /dev/null +++ b/remote/gitea/data/indexers/issues.bleve/rupture_meta.json @@ -0,0 +1 @@ +{"version":5} \ No newline at end of file diff --git a/remote/gitea/data/indexers/issues.bleve/store/root.bolt b/remote/gitea/data/indexers/issues.bleve/store/root.bolt new file mode 100644 index 0000000..a7247b5 Binary files /dev/null and b/remote/gitea/data/indexers/issues.bleve/store/root.bolt differ diff --git a/remote/gitea/data/jwt/private.pem b/remote/gitea/data/jwt/private.pem new file mode 100644 index 0000000..d6d87b6 --- /dev/null +++ b/remote/gitea/data/jwt/private.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDE959xE4ZOnhPD +4iDxlW5dVyXkzpbqADw9+N1D8vzCpvG2937zuQsMQ5BL1Qhzb6fQ8+Mo77TnDFLQ +zXSW2r/zs1QNHZeCuPN9etrlkNKQT6zLFXobRXDeHf/CDvI2l6gJI1s9sMHgurm2 +XBRefVFqJUkeR3CLw2vNn09FxnwGQoTMyUejuA+n6ITII/0RDEgXOdZwKDOEXZom +N7J25YPhsYZ1BGj38tysb/gWNhsFrxVCHPOGiuDll5x5fGMRmb436PvvWktX0Yfg +4NTa6Re5IHpVCSAQR64HD8j0FsGsMDvXhrja5mdRWWjgnbuprtr/f8ei2AEj8CCe +mh7OKb6y1pryLq12hins4cw3PdzS/3+I0VSa5uUpdvZRymjMLJr9tD2UdfJWr6lB +Jp0U0wQ0vBl08kHsTIBOPGJlFtSZtwzP8+C2eAEw20C15qh7sE/NkbS5oyV0XZfB +gtVB0W+FCJNfC5HRuBl3x0Ei0jpFG/QncuHXXripgF/bNcDp+K5iGyJAWMSb9aJp +3lmijC99FGPMLiA4j5QkcR82yIWEVlrgYVEnP9dqaBEt4Rs2Gl1618uj0zdNq6Nj +DyBB6zihf0PnBjvGSG2uqQKz2236MvhxYmPn8vxDuvq7/xdax0So+OGuptbHXhDR +aLNqYCw24Vqg2sXhiuHbdDMrGQUlgQIDAQABAoICAFtlM3y6vJV6UF2Sbgrrddyl +9ZVoLyrBlTKEadd/xr1jzcFCsovRD0lPiINHhLZb1xjkMkHqiJy1YTA2RaVCN9OT +IKs9UfJ8c6+D9FKVkr8X2WwauSAyZp8KeITJfqbKVFR5LXtAq5XlzwrJS0JVEBQa +3QTJzXVs5nbxN01/PkmBUDHeK/nSDKGzCPn9iQ4CDumIEvLUFKOU81RMf+kfssRl +JajitPponPD+u7VCweMvTMPyvyVtB9JBOYA9sZKXLmavG0gDM/a56Tz11o8FndZv +NZSDuXcUa7InJu3sKU4Gy53Ei743LzXWMQ4Y4t1nn4Ly/eWYKV9NqzUs+qJbSHrO +cmFqYr6ypG86TbGk1ji3eDRjmvsUYM/lTNurAh9BPzGz0mTlrUGXc+OPmAU1LD6T +EeArMLHVxunON27T6ytHUV4VDbd8oa1KzsdiKEW71/WsK09kFLkOcGVR30OzLni5 +pQRxUukVX7twcU4hmrheSNB+CMtxACfHRxPalczVCkX/xp2jxwfPGjqr4aSQqQfK ++hMDy5/jc6PrhPd7RSadWPVnFYJaYofWXgi/uU+26jrRmNswetNdvPv9zu5nS1Hs +VFK8Q5zgqsdG+QLEyNWtnz5fwkNMx49cB154V8AR1ZI6XLgbRUL069gNpvHzLl/N +eTb9MKj2d1/b6f3XZxEpAoIBAQDz7tEI0YC1Q5M0EQC0j8zvZy5FLP4NtwE9ZbHt +NsjsHC5xIb3ujhtIje66X/5cnwGy0Bh2QbMDeFXWKlHcw+CiFe1/13kLLUoGetMN +yHXLPCVx3iYNIopZgG8Db7qM7rYkmgVyE6VWyyMzVrFbO/rvtX8BK9jjrDVNRzsF +g1MvGpqSPZu4O3j5BK13U0+eBhTQIvVijvUVCc50ftQHXjhwGfvUsXoggemG0Q89 +UpcKHvsAZbPOtP16HHcTuWk1SyF4qeVN+z1O+gOqebXW8qdzIBsDE2kUcq9bube5 +5dJ8CknbKNYc/dTIMEdKu2zXDK57eL8OUZcT3csuI001d7JfAoIBAQDOtgfRub07 +xNAeH9zK/4/5Smj2D+Ja6C6a37s5fsS1m1KhG0JvD7f4dhABxIf7PY0mvyxSpJt9 +LzyYPzR8B/DoaM5L3aDxcIhsp7JQ+STj1V2SKsNW8M4kRTiEPZh81jzZYtBCxe1o +sLu3lhICusrLtfSSbix2ijMdPDEe1aDMT/P3TVrHUk2u5fMrvZHDB6VqvidXqLyG +R4ifF4eblfbWPtlnNIh4V2vCX4+dfH25oyaO/KKTMuV+0h2+h6AjTOcV2ntXlyZK +/oUDWRPbM6m019DEBoG9VBawyY6aGTM44RHjKBBz6itj938TlseF2qm4BPctfIF+ +gVvKqwSM/PQfAoIBAHCEmlts9+ek0gPUS/T919QeThOOm2mMHsBJZnc7LBbtMOby +X3/ogOFIxvOlT9k9ZzUqE/6Ic6CII1/0iWpB2B4r6y9rHuRu8ZRnl27mJp+mkMcj +Z33rjtGWEp8NLInRmqbrfNOQCFYuwX/u30RsOGXV3E2YAiWSy8tnrevvFbHGncIT +NP8YP8btx24hObp0p6kSVwotUxNvQJIv5nG3nmTnN2h2rRTNmACd8l+g9xauD62x +O+1/QuTOuIaaodL5YukbxS/hUfhaDtLV7XDG4UKTbqJOk8vg0s6Grh7Lyfl5bXPw +NEOPOlVVH61zItNXgCxoGAjszblWN2CC3BxrqBUCggEASt9QMbzvOAjvwRmVZcnv +okI7hnT2bisPRnWnYQnzwjwCT+yJwaSV5F8PKTTAdFY1HEW+jiilUVCcyCCMqChQ +MD5WCtC6DPnP0FtlkULNA+EyxVDL9F/Aqw6PjAarhvwqiirqeGUsuvDY7YRj/a0e +6256qddSL5WbMgmtWRfT6G1FVtwj93JuRN1xmPRPKa9JUUKTCYNK1fBvIgDp04cc +IzockO9MRxqTI5JteIOxHl5kBwKm+F5FFgyRTYPekyq1wQqkBnPvINbT4wSO1qT9 +4U0Shw48TBF7LomzJ0ndbcrIKdlHLFUzZkAtPTEuD+PF+auCxG0GkoXUc7JCMbcl +zQKCAQBuyk/G/9G458QPfvp4if0TNMPuX+yzcmjEcOf4x2vgTA1t6me4nPm4xEF2 +TUO/hECYiNG1DyeN/rRdW6lce9CZMaP9D1Dc1g4fAGfxBBhrBvV7o4h0dffpOTNh +wyKx08C8rmImmvlt3aWyH+2sTe43GScbchSkEid8M/0RpaKp4b61zBhGUgYspKNS +QL+7dTq9iosBTaR+zWKBv6cfOahxXFdVQYkh1woDCLdWX5FCKjV/gD46UFEtMTYf +ABcF+5u2Pbxsf49gUN1wiJBx0o5+81IB34lIscp8wA+3ZRUeDnAbaEPTeoVIP+Of +ohu0miobcXoUkKle/P8TFqStcnW6 +-----END PRIVATE KEY----- diff --git a/remote/gitea/data/queues/common/000002.ldb b/remote/gitea/data/queues/common/000002.ldb new file mode 100644 index 0000000..2945ce5 Binary files /dev/null and b/remote/gitea/data/queues/common/000002.ldb differ diff --git a/remote/gitea/data/queues/common/CURRENT b/remote/gitea/data/queues/common/CURRENT new file mode 100644 index 0000000..7570f19 --- /dev/null +++ b/remote/gitea/data/queues/common/CURRENT @@ -0,0 +1 @@ +MANIFEST-000050 diff --git a/remote/gitea/data/queues/common/CURRENT.bak b/remote/gitea/data/queues/common/CURRENT.bak new file mode 100644 index 0000000..a9ebced --- /dev/null +++ b/remote/gitea/data/queues/common/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000048 diff --git a/remote/gitea/data/queues/common/LOCK b/remote/gitea/data/queues/common/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/remote/gitea/data/queues/common/LOG b/remote/gitea/data/queues/common/LOG new file mode 100644 index 0000000..95f30a0 --- /dev/null +++ b/remote/gitea/data/queues/common/LOG @@ -0,0 +1,223 @@ +=============== Mar 4, 2026 (CST) =============== +23:25:27.295431 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +23:25:27.296428 db@open opening +23:25:27.297425 version@stat F·[] S·0B[] Sc·[] +23:25:27.297425 db@janitor F·2 G·0 +23:25:27.297425 db@open done T·997.1µs +=============== Mar 5, 2026 (CST) =============== +00:58:48.117008 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +00:58:48.118039 version@stat F·[] S·0B[] Sc·[] +00:58:48.118039 db@open opening +00:58:48.118039 journal@recovery F·1 +00:58:48.118039 journal@recovery recovering @1 +00:58:48.119011 memdb@flush created L0@2 N·28 S·574B "act..igh,v28":"web..low,v19" +00:58:48.120008 version@stat F·[1] S·574B[574B] Sc·[0.25] +00:58:48.122002 db@janitor F·3 G·0 +00:58:48.122002 db@open done T·3.9631ms +=============== Mar 5, 2026 (CST) =============== +16:44:08.552284 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +16:44:08.552284 version@stat F·[1] S·574B[574B] Sc·[0.25] +16:44:08.552284 db@open opening +16:44:08.552284 journal@recovery F·1 +16:44:08.552284 journal@recovery recovering @3 +16:44:08.553282 version@stat F·[1] S·574B[574B] Sc·[0.25] +16:44:08.555630 db@janitor F·3 G·0 +16:44:08.555630 db@open done T·3.3467ms +=============== Mar 5, 2026 (CST) =============== +16:58:44.704290 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +16:58:44.704290 version@stat F·[1] S·574B[574B] Sc·[0.25] +16:58:44.704290 db@open opening +16:58:44.704290 journal@recovery F·1 +16:58:44.704290 journal@recovery recovering @5 +16:58:44.705289 version@stat F·[1] S·574B[574B] Sc·[0.25] +16:58:44.709300 db@janitor F·3 G·0 +16:58:44.709300 db@open done T·5.0092ms +=============== Mar 5, 2026 (CST) =============== +16:58:55.013041 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +16:58:55.014038 version@stat F·[1] S·574B[574B] Sc·[0.25] +16:58:55.014038 db@open opening +16:58:55.014038 journal@recovery F·1 +16:58:55.014038 journal@recovery recovering @7 +16:58:55.014038 version@stat F·[1] S·574B[574B] Sc·[0.25] +16:58:55.019025 db@janitor F·3 G·0 +16:58:55.019025 db@open done T·4.9869ms +=============== Mar 5, 2026 (CST) =============== +17:01:48.341808 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:01:48.341808 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:01:48.341808 db@open opening +17:01:48.341808 journal@recovery F·1 +17:01:48.341808 journal@recovery recovering @9 +17:01:48.342805 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:01:48.346766 db@janitor F·3 G·0 +17:01:48.346766 db@open done T·4.9576ms +=============== Mar 5, 2026 (CST) =============== +17:05:22.244372 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:05:22.245370 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:05:22.245370 db@open opening +17:05:22.245370 journal@recovery F·1 +17:05:22.245370 journal@recovery recovering @11 +17:05:22.245370 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:05:22.250356 db@janitor F·3 G·0 +17:05:22.250356 db@open done T·4.9865ms +=============== Mar 5, 2026 (CST) =============== +17:15:26.696181 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:15:26.696181 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:15:26.696181 db@open opening +17:15:26.696181 journal@recovery F·1 +17:15:26.696181 journal@recovery recovering @13 +17:15:26.696181 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:15:26.701109 db@janitor F·3 G·0 +17:15:26.701109 db@open done T·4.9277ms +=============== Mar 5, 2026 (CST) =============== +17:42:26.549391 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:42:26.550343 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:42:26.550343 db@open opening +17:42:26.550343 journal@recovery F·1 +17:42:26.550343 journal@recovery recovering @15 +17:42:26.552338 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:42:26.554337 db@janitor F·3 G·0 +17:42:26.554337 db@open done T·3.9934ms +=============== Mar 5, 2026 (CST) =============== +17:53:41.022244 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:53:41.022244 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:53:41.022244 db@open opening +17:53:41.022244 journal@recovery F·1 +17:53:41.022244 journal@recovery recovering @17 +17:53:41.023231 version@stat F·[1] S·574B[574B] Sc·[0.25] +17:53:41.026274 db@janitor F·3 G·0 +17:53:41.026274 db@open done T·4.0304ms +=============== Mar 5, 2026 (CST) =============== +18:06:44.931577 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:06:44.932090 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:06:44.932090 db@open opening +18:06:44.932090 journal@recovery F·1 +18:06:44.932602 journal@recovery recovering @19 +18:06:44.933113 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:06:44.935926 db@janitor F·3 G·0 +18:06:44.935926 db@open done T·3.8362ms +=============== Mar 5, 2026 (CST) =============== +18:17:10.357320 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:17:10.357320 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:17:10.357833 db@open opening +18:17:10.357841 journal@recovery F·1 +18:17:10.357841 journal@recovery recovering @21 +18:17:10.357841 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:17:10.361450 db@janitor F·3 G·0 +18:17:10.361450 db@open done T·3.6088ms +=============== Mar 5, 2026 (CST) =============== +18:20:47.461717 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:20:47.462247 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:20:47.462247 db@open opening +18:20:47.462247 journal@recovery F·1 +18:20:47.462247 journal@recovery recovering @23 +18:20:47.464218 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:20:47.466420 db@janitor F·3 G·0 +18:20:47.466420 db@open done T·4.1729ms +=============== Mar 5, 2026 (CST) =============== +18:24:07.472215 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:24:07.473212 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:24:07.473212 db@open opening +18:24:07.473212 journal@recovery F·1 +18:24:07.473212 journal@recovery recovering @25 +18:24:07.473212 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:24:07.476910 db@janitor F·3 G·0 +18:24:07.476910 db@open done T·3.6976ms +=============== Mar 5, 2026 (CST) =============== +18:41:46.328705 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:41:46.329704 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:41:46.329704 db@open opening +18:41:46.329704 journal@recovery F·1 +18:41:46.329704 journal@recovery recovering @27 +18:41:46.329704 version@stat F·[1] S·574B[574B] Sc·[0.25] +18:41:46.334694 db@janitor F·3 G·0 +18:41:46.334694 db@open done T·4.9906ms +=============== Mar 5, 2026 (CST) =============== +20:17:07.605654 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:17:07.606653 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:17:07.606653 db@open opening +20:17:07.606653 journal@recovery F·1 +20:17:07.606653 journal@recovery recovering @29 +20:17:07.606653 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:17:07.609645 db@janitor F·3 G·0 +20:17:07.609645 db@open done T·2.9923ms +=============== Mar 5, 2026 (CST) =============== +20:18:45.285721 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:18:45.286227 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:18:45.286227 db@open opening +20:18:45.286227 journal@recovery F·1 +20:18:45.286227 journal@recovery recovering @31 +20:18:45.286227 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:18:45.290050 db@janitor F·3 G·0 +20:18:45.290050 db@open done T·3.823ms +=============== Mar 5, 2026 (CST) =============== +20:24:14.214040 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:24:14.215038 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:24:14.215038 db@open opening +20:24:14.215038 journal@recovery F·1 +20:24:14.217038 journal@recovery recovering @33 +20:24:14.217038 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:24:14.221083 db@janitor F·3 G·0 +20:24:14.221083 db@open done T·6.0448ms +=============== Mar 5, 2026 (CST) =============== +20:27:37.450756 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:27:37.451263 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:27:37.451263 db@open opening +20:27:37.451263 journal@recovery F·1 +20:27:37.451263 journal@recovery recovering @35 +20:27:37.451263 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:27:37.454842 db@janitor F·3 G·0 +20:27:37.454842 db@open done T·3.579ms +=============== Mar 5, 2026 (CST) =============== +20:30:19.881992 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:30:19.881992 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:30:19.881992 db@open opening +20:30:19.881992 journal@recovery F·1 +20:30:19.881992 journal@recovery recovering @37 +20:30:19.882990 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:30:19.884984 db@janitor F·3 G·0 +20:30:19.884984 db@open done T·2.9921ms +=============== Mar 5, 2026 (CST) =============== +20:33:21.085745 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:33:21.086742 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:33:21.086742 db@open opening +20:33:21.086742 journal@recovery F·1 +20:33:21.086742 journal@recovery recovering @39 +20:33:21.088251 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:33:21.093539 db@janitor F·3 G·0 +20:33:21.093539 db@open done T·6.7975ms +=============== Mar 5, 2026 (CST) =============== +20:35:21.489745 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:35:21.490699 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:35:21.490699 db@open opening +20:35:21.490699 journal@recovery F·1 +20:35:21.490699 journal@recovery recovering @41 +20:35:21.491220 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:35:21.495194 db@janitor F·3 G·0 +20:35:21.495194 db@open done T·4.4953ms +=============== Mar 5, 2026 (CST) =============== +20:42:06.364290 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:42:06.364290 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:42:06.364290 db@open opening +20:42:06.364796 journal@recovery F·1 +20:42:06.364796 journal@recovery recovering @43 +20:42:06.364796 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:42:06.369434 db@janitor F·3 G·0 +20:42:06.369434 db@open done T·5.1446ms +=============== Mar 5, 2026 (CST) =============== +20:46:58.679386 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:46:58.679386 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:46:58.679386 db@open opening +20:46:58.679386 journal@recovery F·1 +20:46:58.679386 journal@recovery recovering @45 +20:46:58.680383 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:46:58.683452 db@janitor F·3 G·0 +20:46:58.683452 db@open done T·4.0659ms +=============== Mar 5, 2026 (CST) =============== +20:49:54.485040 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +20:49:54.485040 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:49:54.485040 db@open opening +20:49:54.485040 journal@recovery F·1 +20:49:54.485040 journal@recovery recovering @47 +20:49:54.486036 version@stat F·[1] S·574B[574B] Sc·[0.25] +20:49:54.488644 db@janitor F·3 G·0 +20:49:54.488644 db@open done T·3.6037ms diff --git a/remote/gitea/data/queues/common/MANIFEST-000050 b/remote/gitea/data/queues/common/MANIFEST-000050 new file mode 100644 index 0000000..f4eb661 Binary files /dev/null and b/remote/gitea/data/queues/common/MANIFEST-000050 differ diff --git a/remote/gitea/data/sessions/2/1/21b593c37c880f97 b/remote/gitea/data/sessions/2/1/21b593c37c880f97 new file mode 100644 index 0000000..0b9cdfb Binary files /dev/null and b/remote/gitea/data/sessions/2/1/21b593c37c880f97 differ diff --git a/remote/gitea/data/sessions/3/5/350a92c671df51b4 b/remote/gitea/data/sessions/3/5/350a92c671df51b4 new file mode 100644 index 0000000..9ebfaa1 Binary files /dev/null and b/remote/gitea/data/sessions/3/5/350a92c671df51b4 differ diff --git a/remote/gitea/data/sessions/f/d/fd149c2b9f94a63e b/remote/gitea/data/sessions/f/d/fd149c2b9f94a63e new file mode 100644 index 0000000..528c2e0 Binary files /dev/null and b/remote/gitea/data/sessions/f/d/fd149c2b9f94a63e differ diff --git a/remote/gitea/gitea.exe b/remote/gitea/gitea.exe new file mode 100644 index 0000000..56aca99 Binary files /dev/null and b/remote/gitea/gitea.exe differ diff --git a/remote/nssm/nssm.exe b/remote/nssm/nssm.exe new file mode 100644 index 0000000..6ccfe3c Binary files /dev/null and b/remote/nssm/nssm.exe differ diff --git a/remote/package-lock.json b/remote/package-lock.json new file mode 100644 index 0000000..14da4ab --- /dev/null +++ b/remote/package-lock.json @@ -0,0 +1,3076 @@ +{ + "name": "remote-screen-monitor", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "remote-screen-monitor", + "version": "2.0.0", + "license": "ISC", + "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "bcryptjs": "^2.4.3", + "config": "^3.3.12", + "cookie-parser": "^1.4.7", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "h264-live-player": "^1.3.1", + "jsonwebtoken": "^9.0.2", + "multer": "^2.1.0", + "winston": "^3.19.0", + "ws": "^8.19.0" + }, + "bin": { + "remote-screen-monitor": "src/index.js" + }, + "devDependencies": { + "pkg": "^5.8.1" + } + }, + "node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@ffmpeg-installer/darwin-arm64": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz", + "integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffmpeg-installer/darwin-x64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz", + "integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "license": "LGPL-2.1", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffmpeg-installer/ffmpeg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz", + "integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==", + "license": "LGPL-2.1", + "optionalDependencies": { + "@ffmpeg-installer/darwin-arm64": "4.1.5", + "@ffmpeg-installer/darwin-x64": "4.1.0", + "@ffmpeg-installer/linux-arm": "4.1.3", + "@ffmpeg-installer/linux-arm64": "4.1.4", + "@ffmpeg-installer/linux-ia32": "4.1.0", + "@ffmpeg-installer/linux-x64": "4.1.0", + "@ffmpeg-installer/win32-ia32": "4.1.0", + "@ffmpeg-installer/win32-x64": "4.1.0" + } + }, + "node_modules/@ffmpeg-installer/linux-arm": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz", + "integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==", + "cpu": [ + "arm" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-arm64": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz", + "integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-ia32": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz", + "integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==", + "cpu": [ + "ia32" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-x64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz", + "integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/win32-ia32": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz", + "integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==", + "cpu": [ + "ia32" + ], + "license": "GPLv3", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ffmpeg-installer/win32-x64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz", + "integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==", + "cpu": [ + "x64" + ], + "license": "GPLv3", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@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/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/config": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", + "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/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/h264-live-player": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/h264-live-player/-/h264-live-player-1.3.1.tgz", + "integrity": "sha512-dH3PS7WEsQIbxxIlMHA8piFJSHDJvVNw4JbUOpiqPWigULV0z90hlMwQ5MARl5TYgCHl/DJ0tzUl7LyyGiGloQ==", + "license": "ISC", + "dependencies": { + "debug": "^2.3.2", + "sylvester.js": "^0.1.1", + "uclass": "^2.4.0" + } + }, + "node_modules/h264-live-player/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/h264-live-player/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mout": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/mout/-/mout-1.2.4.tgz", + "integrity": "sha512-mZb9uOruMWgn/fw28DG4/yE3Kehfk1zKCLhuDU2O3vlKdnBBr4XaOCqVTflJ5aODavGUPqFHZgrFX3NJVuxGhQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/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/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve/node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-meter/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sylvester.js": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sylvester.js/-/sylvester.js-0.1.1.tgz", + "integrity": "sha512-BDJneP4tUmzFR4jrgANtuozDWHBnVyYW7aMTZsnp1zLUhv2xsk4K3sEE0YlJguSCz2lST7KLq3GYLNFacA9SmQ==", + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uclass": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/uclass/-/uclass-2.4.0.tgz", + "integrity": "sha512-xmfrzLdmffIu2TwQjXvo1XAb0LM5SvPqp8CGbCKOPQhptImDoul0pKFQl9chbXH4v40di3gT24H1Oun70Z6c3g==", + "license": "MIT", + "dependencies": { + "mout": "^1.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + } + } +} diff --git a/remote/package.json b/remote/package.json new file mode 100644 index 0000000..887bea0 --- /dev/null +++ b/remote/package.json @@ -0,0 +1,48 @@ +{ + "name": "remote-screen-monitor", + "version": "2.0.0", + "description": "Remote screen monitoring and control system with mouse and keyboard support", + "main": "src/index.js", + "bin": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node src/index.js", + "build": "pkg . --targets node18-win-x64 --output dist/remote-screen-monitor.exe && npm run build:copy-assets", + "build:copy-assets": "node -e \"const fs=require('fs');const path=require('path');const dist='dist';if(!fs.existsSync(dist+'/public'))fs.cpSync('public',dist+'/public',{recursive:true});if(!fs.existsSync(dist+'/config'))fs.cpSync('config',dist+'/config',{recursive:true});if(!fs.existsSync(dist+'/frp'))fs.cpSync('frp',dist+'/frp',{recursive:true});fs.cpSync('node_modules/@ffmpeg-installer/win32-x64/ffmpeg.exe',dist+'/ffmpeg.exe');\"", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "pkg": { + "assets": [ + "node_modules/@ffmpeg-installer/**/*" + ], + "scripts": [ + "src/**/*.js" + ] + }, + "keywords": [ + "remote-screen", + "screen-sharing", + "remote-control", + "mouse-control", + "keyboard-control", + "streaming" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "bcryptjs": "^2.4.3", + "config": "^3.3.12", + "cookie-parser": "^1.4.7", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "h264-live-player": "^1.3.1", + "jsonwebtoken": "^9.0.2", + "multer": "^2.1.0", + "winston": "^3.19.0", + "ws": "^8.19.0" + }, + "devDependencies": { + "pkg": "^5.8.1" + } +} diff --git a/remote/public/css/main.css b/remote/public/css/main.css new file mode 100644 index 0000000..5061f30 --- /dev/null +++ b/remote/public/css/main.css @@ -0,0 +1,339 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: #000; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +#video-canvas { + display: block; + max-width: 100vw; + max-height: 100vh; + object-fit: contain; +} + +#file-transfer-btn { + position: fixed; + top: 10px; + right: 10px; + z-index: 100; + padding: 8px 16px; + background: rgba(0, 120, 215, 0.9); + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background 0.2s; +} + +#file-transfer-btn:hover { + background: rgba(0, 120, 215, 1); +} + +#file-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.9); + width: 90%; + max-width: 900px; + max-height: 80vh; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + display: flex; + flex-direction: column; +} + +#file-panel.visible { + opacity: 1; + visibility: visible; + transform: translate(-50%, -50%) scale(1); +} + +.file-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #252525; + border-bottom: 1px solid #333; + border-radius: 8px 8px 0 0; +} + +.file-panel-header span { + color: #fff; + font-size: 16px; + font-weight: 500; +} + +.close-btn { + background: none; + border: none; + color: #888; + font-size: 24px; + cursor: pointer; + padding: 0 4px; + line-height: 1; +} + +.close-btn:hover { + color: #fff; +} + +.file-panel-body { + display: flex; + gap: 16px; + padding: 16px; + flex: 1; + overflow: hidden; +} + +.file-browser { + flex: 1; + display: flex; + flex-direction: column; + background: #222; + border-radius: 6px; + overflow: hidden; +} + +.browser-header { + padding: 10px 12px; + background: #2a2a2a; + border-bottom: 1px solid #333; +} + +.browser-title { + color: #fff; + font-size: 14px; + font-weight: 500; + display: block; + margin-bottom: 8px; +} + +.path-nav { + display: flex; + gap: 8px; + align-items: center; +} + +.nav-btn { + padding: 4px 10px; + background: #333; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; +} + +.nav-btn:hover { + background: #444; +} + +.path-input { + flex: 1; + padding: 6px 10px; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 4px; + color: #aaa; + font-size: 12px; +} + +.file-list { + flex: 1; + overflow-y: auto; + padding: 8px; + min-height: 200px; + max-height: 300px; +} + +.file-item { + display: flex; + align-items: center; + padding: 8px 10px; + border-radius: 4px; + cursor: pointer; + transition: background 0.15s; +} + +.file-item:hover { + background: #333; +} + +.file-item.selected { + background: rgba(0, 120, 215, 0.3); +} + +.file-icon { + margin-right: 8px; + font-size: 16px; +} + +.file-name { + flex: 1; + color: #ddd; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-size { + color: #888; + font-size: 12px; + margin-left: 10px; +} + +.remove-btn { + background: none; + border: none; + color: #666; + font-size: 16px; + cursor: pointer; + padding: 0 4px; + margin-left: 8px; +} + +.remove-btn:hover { + color: #f44336; +} + +.browser-actions { + display: flex; + gap: 8px; + padding: 10px 12px; + background: #2a2a2a; + border-top: 1px solid #333; +} + +.action-btn { + flex: 1; + padding: 8px 12px; + background: #333; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background 0.2s; +} + +.action-btn:hover:not(:disabled) { + background: #444; +} + +.action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.action-btn.primary { + background: #0078d7; +} + +.action-btn.primary:hover:not(:disabled) { + background: #006cbd; +} + +.action-btn.danger { + background: #d32f2f; +} + +.action-btn.danger:hover:not(:disabled) { + background: #b71c1c; +} + +.transfer-list { + padding: 10px 16px; + background: #252525; + border-top: 1px solid #333; + border-radius: 0 0 8px 8px; + max-height: 120px; + overflow-y: auto; +} + +.transfer-item { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 0; + font-size: 12px; +} + +.transfer-type { + color: #888; + font-size: 14px; +} + +.transfer-name { + color: #ddd; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.transfer-progress { + width: 100px; + height: 4px; + background: #333; + border-radius: 2px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: #0078d7; + transition: width 0.2s; +} + +.transfer-percent { + color: #888; + width: 35px; + text-align: right; +} + +.transfer-item.completed .progress-bar { + background: #4caf50; +} + +.transfer-item.failed .progress-bar { + background: #f44336; +} + +.file-list::-webkit-scrollbar, +.transfer-list::-webkit-scrollbar { + width: 6px; +} + +.file-list::-webkit-scrollbar-track, +.transfer-list::-webkit-scrollbar-track { + background: #1a1a1a; +} + +.file-list::-webkit-scrollbar-thumb, +.transfer-list::-webkit-scrollbar-thumb { + background: #444; + border-radius: 3px; +} + +.file-list::-webkit-scrollbar-thumb:hover, +.transfer-list::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/remote/public/index.html b/remote/public/index.html new file mode 100644 index 0000000..3f1dab6 --- /dev/null +++ b/remote/public/index.html @@ -0,0 +1,150 @@ + + + + + + xc-remote + + + + + + + + + + + + + + + diff --git a/remote/public/js/app.js b/remote/public/js/app.js new file mode 100644 index 0000000..5694a1f --- /dev/null +++ b/remote/public/js/app.js @@ -0,0 +1,43 @@ +(function() { + const password = getCookie('auth') || ''; + + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsHost = window.location.hostname; + const wsPort = window.location.port; + const wsUrlBase = wsPort ? `${wsProtocol}//${wsHost}:${wsPort}/ws` : `${wsProtocol}//${wsHost}/ws`; + const WS_URL = password ? `${wsUrlBase}?password=${encodeURIComponent(password)}` : wsUrlBase; + + let videoPlayer = null; + let inputHandler = null; + + function init() { + videoPlayer = new VideoPlayer('video-canvas', WS_URL); + videoPlayer.init(); + + inputHandler = new InputHandler(videoPlayer.getCanvas(), { + wsUrl: WS_URL + }); + inputHandler.init(); + + console.log('xc-remote initialized'); + } + + function destroy() { + if (inputHandler) { + inputHandler.destroy(); + inputHandler = null; + } + if (videoPlayer) { + videoPlayer.destroy(); + videoPlayer = null; + } + } + + window.addEventListener('beforeunload', destroy); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/remote/public/js/file-panel.js b/remote/public/js/file-panel.js new file mode 100644 index 0000000..004bb46 --- /dev/null +++ b/remote/public/js/file-panel.js @@ -0,0 +1,500 @@ +class FilePanel { + constructor() { + this.isVisible = false; + this.localPath = ''; + this.remotePath = ''; + this.localFiles = []; + this.remoteFiles = []; + this.selectedLocal = null; + this.selectedRemote = null; + this.transfers = []; + this.chunkSize = 5 * 1024 * 1024; + + this.init(); + } + + init() { + this.createPanel(); + this.bindEvents(); + } + + createPanel() { + const panel = document.createElement('div'); + panel.id = 'file-panel'; + panel.innerHTML = ` +
+ 文件传输 + +
+
+
+
+ 本地文件 +
+ + +
+
+
+
+ + +
+
+
+
+ 远程文件 +
+ + +
+
+
+
+ + +
+
+
+
+ + `; + document.body.appendChild(panel); + this.panel = panel; + } + + bindEvents() { + document.getElementById('close-panel').addEventListener('click', () => this.hide()); + + document.getElementById('local-up').addEventListener('click', () => this.navigateLocalUp()); + document.getElementById('remote-up').addEventListener('click', () => this.navigateRemoteUp()); + + document.getElementById('select-local-file').addEventListener('click', () => { + document.getElementById('file-input').click(); + }); + + document.getElementById('file-input').addEventListener('change', (e) => { + this.handleLocalFileSelect(e.target.files); + }); + + document.getElementById('upload-btn').addEventListener('click', () => this.uploadSelected()); + document.getElementById('download-btn').addEventListener('click', () => this.downloadSelected()); + document.getElementById('delete-btn').addEventListener('click', () => this.deleteSelected()); + + document.getElementById('remote-files').addEventListener('dblclick', (e) => { + const item = e.target.closest('.file-item'); + if (item && item.dataset.isDirectory === 'true') { + this.navigateRemote(item.dataset.name); + } + }); + + document.getElementById('remote-files').addEventListener('click', (e) => { + const item = e.target.closest('.file-item'); + if (item) { + this.selectRemote(item.dataset.name, item.dataset.isDirectory === 'true'); + } + }); + } + + show() { + this.isVisible = true; + this.panel.classList.add('visible'); + this.refreshRemote(); + } + + hide() { + this.isVisible = false; + this.panel.classList.remove('visible'); + } + + toggle() { + if (this.isVisible) { + this.hide(); + } else { + this.show(); + } + } + + async refreshRemote() { + try { + const res = await fetch(`/api/files/browse?path=${encodeURIComponent(this.remotePath)}`); + const data = await res.json(); + + if (data.error) { + console.error('Browse error:', data.error); + return; + } + + this.remoteFiles = data.items || []; + this.remotePath = data.currentPath || ''; + + document.getElementById('remote-path').value = this.remotePath || '/'; + this.renderRemoteFiles(); + } catch (error) { + console.error('Failed to refresh remote:', error); + } + } + + renderRemoteFiles() { + const container = document.getElementById('remote-files'); + + if (this.remotePath) { + container.innerHTML = `
+ 📁 + .. +
`; + } else { + container.innerHTML = ''; + } + + this.remoteFiles.forEach(item => { + const div = document.createElement('div'); + div.className = `file-item ${item.isDirectory ? 'directory' : 'file'}`; + div.dataset.name = item.name; + div.dataset.isDirectory = item.isDirectory; + div.innerHTML = ` + ${item.isDirectory ? '📁' : this.getFileIcon(item.name)} + ${item.name} + ${item.isDirectory ? '' : this.formatSize(item.size)} + `; + container.appendChild(div); + }); + + this.selectedRemote = null; + this.updateRemoteActions(); + } + + selectRemote(name, isDirectory) { + const items = document.querySelectorAll('#remote-files .file-item'); + items.forEach(item => item.classList.remove('selected')); + + const selected = document.querySelector(`#remote-files .file-item[data-name="${name}"]`); + if (selected) { + selected.classList.add('selected'); + this.selectedRemote = { name, isDirectory }; + } + + this.updateRemoteActions(); + } + + updateRemoteActions() { + const downloadBtn = document.getElementById('download-btn'); + const deleteBtn = document.getElementById('delete-btn'); + + if (this.selectedRemote && !this.selectedRemote.isDirectory) { + downloadBtn.disabled = false; + deleteBtn.disabled = false; + } else { + downloadBtn.disabled = true; + deleteBtn.disabled = true; + } + } + + navigateRemote(name) { + if (name === '..') { + this.remotePath = this.remotePath.split('/').slice(0, -1).join('/'); + } else { + this.remotePath = this.remotePath ? `${this.remotePath}/${name}` : name; + } + this.refreshRemote(); + } + + navigateRemoteUp() { + if (this.remotePath) { + this.remotePath = this.remotePath.split('/').slice(0, -1).join('/'); + this.refreshRemote(); + } + } + + handleLocalFileSelect(files) { + if (files.length === 0) return; + + this.localFiles = Array.from(files).map(file => ({ + name: file.name, + size: file.size, + file: file, + isDirectory: false + })); + + this.renderLocalFiles(); + } + + renderLocalFiles() { + const container = document.getElementById('local-files'); + container.innerHTML = ''; + + this.localFiles.forEach((item, index) => { + const div = document.createElement('div'); + div.className = 'file-item file'; + div.dataset.index = index; + div.innerHTML = ` + ${this.getFileIcon(item.name)} + ${item.name} + ${this.formatSize(item.size)} + + `; + container.appendChild(div); + }); + + container.querySelectorAll('.remove-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const index = parseInt(btn.dataset.index); + this.localFiles.splice(index, 1); + this.renderLocalFiles(); + this.updateLocalActions(); + }); + }); + + container.querySelectorAll('.file-item').forEach(item => { + item.addEventListener('click', () => { + container.querySelectorAll('.file-item').forEach(i => i.classList.remove('selected')); + item.classList.add('selected'); + this.selectedLocal = parseInt(item.dataset.index); + this.updateLocalActions(); + }); + }); + + this.updateLocalActions(); + } + + updateLocalActions() { + const uploadBtn = document.getElementById('upload-btn'); + uploadBtn.disabled = this.localFiles.length === 0; + } + + navigateLocalUp() { + this.localFiles = []; + this.selectedLocal = null; + this.renderLocalFiles(); + this.updateLocalActions(); + } + + async uploadSelected() { + if (this.localFiles.length === 0) return; + + for (const item of this.localFiles) { + await this.uploadFile(item); + } + + this.localFiles = []; + this.selectedLocal = null; + this.renderLocalFiles(); + this.updateLocalActions(); + this.refreshRemote(); + } + + async uploadFile(item) { + const transferId = Date.now(); + this.addTransfer(transferId, item.name, 'upload', item.size); + + try { + const totalChunks = Math.ceil(item.size / this.chunkSize); + + const startRes = await fetch('/api/files/upload/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filename: item.name, + totalChunks, + fileSize: item.size + }) + }); + + const { fileId } = await startRes.json(); + + for (let i = 0; i < totalChunks; i++) { + const start = i * this.chunkSize; + const end = Math.min(start + this.chunkSize, item.size); + const chunk = item.file.slice(start, end); + + const formData = new FormData(); + formData.append('fileId', fileId); + formData.append('chunkIndex', i); + formData.append('chunk', chunk); + + await fetch('/api/files/upload/chunk', { + method: 'POST', + body: formData + }); + + const progress = Math.round(((i + 1) / totalChunks) * 100); + this.updateTransfer(transferId, progress); + } + + const remoteFilePath = this.remotePath ? `${this.remotePath}/${item.name}` : item.name; + + await fetch('/api/files/upload/merge', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + fileId, + totalChunks, + filename: remoteFilePath + }) + }); + + this.completeTransfer(transferId); + } catch (error) { + console.error('Upload failed:', error); + this.failTransfer(transferId, error.message); + } + } + + async downloadSelected() { + if (!this.selectedRemote || this.selectedRemote.isDirectory) return; + + const filename = this.selectedRemote.name; + const remoteFilePath = this.remotePath ? `${this.remotePath}/${filename}` : filename; + + const transferId = Date.now(); + this.addTransfer(transferId, filename, 'download', 0); + + try { + const res = await fetch(`/api/files/${encodeURIComponent(remoteFilePath)}`); + + if (!res.ok) { + throw new Error('Download failed'); + } + + const contentLength = parseInt(res.headers.get('Content-Length') || '0'); + this.updateTransferSize(transferId, contentLength); + + const reader = res.body.getReader(); + const chunks = []; + let received = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + received += value.length; + + const progress = contentLength > 0 ? Math.round((received / contentLength) * 100) : 0; + this.updateTransfer(transferId, progress); + } + + const blob = new Blob(chunks); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + + this.completeTransfer(transferId); + } catch (error) { + console.error('Download failed:', error); + this.failTransfer(transferId, error.message); + } + } + + async deleteSelected() { + if (!this.selectedRemote || this.selectedRemote.isDirectory) return; + + if (!confirm(`确定要删除 "${this.selectedRemote.name}" 吗?`)) return; + + const remoteFilePath = this.remotePath ? `${this.remotePath}/${this.selectedRemote.name}` : this.selectedRemote.name; + + try { + const res = await fetch(`/api/files/${encodeURIComponent(remoteFilePath)}`, { + method: 'DELETE' + }); + + if (res.ok) { + this.refreshRemote(); + } + } catch (error) { + console.error('Delete failed:', error); + } + } + + addTransfer(id, name, type, size) { + const transfer = { id, name, type, size, progress: 0, status: 'transferring' }; + this.transfers.push(transfer); + this.renderTransfers(); + } + + updateTransfer(id, progress) { + const transfer = this.transfers.find(t => t.id === id); + if (transfer) { + transfer.progress = progress; + this.renderTransfers(); + } + } + + updateTransferSize(id, size) { + const transfer = this.transfers.find(t => t.id === id); + if (transfer) { + transfer.size = size; + this.renderTransfers(); + } + } + + completeTransfer(id) { + const transfer = this.transfers.find(t => t.id === id); + if (transfer) { + transfer.status = 'completed'; + transfer.progress = 100; + this.renderTransfers(); + + setTimeout(() => { + this.transfers = this.transfers.filter(t => t.id !== id); + this.renderTransfers(); + }, 3000); + } + } + + failTransfer(id, error) { + const transfer = this.transfers.find(t => t.id === id); + if (transfer) { + transfer.status = 'failed'; + transfer.error = error; + this.renderTransfers(); + } + } + + renderTransfers() { + const container = document.getElementById('transfer-list'); + + if (this.transfers.length === 0) { + container.innerHTML = ''; + return; + } + + container.innerHTML = this.transfers.map(t => ` +
+ ${t.type === 'upload' ? '↑' : '↓'} + ${t.name} +
+
+
+ ${t.progress}% +
+ `).join(''); + } + + formatSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + } + + getFileIcon(filename) { + const ext = filename.split('.').pop().toLowerCase(); + const icons = { + 'txt': '📄', 'doc': '📄', 'docx': '📄', 'pdf': '📄', + 'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', + 'mp3': '🎵', 'wav': '🎵', 'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬', + 'zip': '📦', 'rar': '📦', '7z': '📦', + 'exe': '⚙️', 'msi': '⚙️', + 'js': '📜', 'ts': '📜', 'py': '📜', 'html': '📜', 'css': '📜' + }; + return icons[ext] || '📄'; + } +} + +window.FilePanel = FilePanel; diff --git a/remote/public/js/input.js b/remote/public/js/input.js new file mode 100644 index 0000000..a93dcab --- /dev/null +++ b/remote/public/js/input.js @@ -0,0 +1,229 @@ +class InputHandler { + constructor(canvas, options = {}) { + this.canvas = canvas; + this.wsUrl = options.wsUrl; + this.screenWidth = options.screenWidth || 1280; + this.screenHeight = options.screenHeight || 720; + + this.ws = null; + this.wsReady = false; + + this.lastMoveTime = 0; + this.MOVE_THROTTLE_MS = 33; + this.pendingMove = null; + this.moveThrottleTimer = null; + + this.pressedKeys = new Set(); + + this.codeToKey = { + 'Enter': 'enter', 'Backspace': 'backspace', 'Tab': 'tab', + 'Escape': 'escape', 'Delete': 'delete', 'Home': 'home', + 'End': 'end', 'PageUp': 'pageup', 'PageDown': 'pagedown', + 'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right', + 'F1': 'f1', 'F2': 'f2', 'F3': 'f3', 'F4': 'f4', + 'F5': 'f5', 'F6': 'f6', 'F7': 'f7', 'F8': 'f8', + 'F9': 'f9', 'F10': 'f10', 'F11': 'f11', 'F12': 'f12', + 'ControlLeft': 'ctrl', 'ControlRight': 'ctrl', + 'AltLeft': 'alt', 'AltRight': 'alt', + 'ShiftLeft': 'shift', 'ShiftRight': 'shift', + 'MetaLeft': 'win', 'MetaRight': 'win', + 'Space': 'space', + 'Digit0': '0', 'Digit1': '1', 'Digit2': '2', 'Digit3': '3', 'Digit4': '4', + 'Digit5': '5', 'Digit6': '6', 'Digit7': '7', 'Digit8': '8', 'Digit9': '9', + 'KeyA': 'a', 'KeyB': 'b', 'KeyC': 'c', 'KeyD': 'd', 'KeyE': 'e', + 'KeyF': 'f', 'KeyG': 'g', 'KeyH': 'h', 'KeyI': 'i', 'KeyJ': 'j', + 'KeyK': 'k', 'KeyL': 'l', 'KeyM': 'm', 'KeyN': 'n', 'KeyO': 'o', + 'KeyP': 'p', 'KeyQ': 'q', 'KeyR': 'r', 'KeyS': 's', 'KeyT': 't', + 'KeyU': 'u', 'KeyV': 'v', 'KeyW': 'w', 'KeyX': 'x', 'KeyY': 'y', 'KeyZ': 'z', + 'Comma': ',', 'Period': '.', 'Slash': '/', 'Semicolon': ';', + 'Quote': "'", 'BracketLeft': '[', 'BracketRight': ']', + 'Backslash': '\\', 'Minus': '-', 'Equal': '=', 'Backquote': '`' + }; + + this._boundHandlers = { + mousemove: this._handleMouseMove.bind(this), + mousedown: this._handleMouseDown.bind(this), + mouseup: this._handleMouseUp.bind(this), + contextmenu: this._handleContextMenu.bind(this), + wheel: this._handleWheel.bind(this), + keydown: this._handleKeyDown.bind(this), + keyup: this._handleKeyUp.bind(this), + blur: this._handleBlur.bind(this) + }; + } + + init() { + this._initWebSocket(); + this._bindEvents(); + return this; + } + + _initWebSocket() { + this.ws = createReconnectingWebSocket(this.wsUrl, { + onOpen: () => { + this.wsReady = true; + console.log('Input WebSocket connected'); + }, + onClose: () => { + this.wsReady = false; + console.log('Input WebSocket disconnected'); + }, + onMessage: (e) => { + try { + const msg = JSON.parse(e.data); + if (msg.type === 'screenInfo') { + this.screenWidth = msg.width; + this.screenHeight = msg.height; + console.log('Screen resolution:', this.screenWidth, 'x', this.screenHeight); + } + } catch (err) {} + }, + onError: (err) => { + console.error('Input WebSocket error:', err); + } + }); + } + + _bindEvents() { + this.canvas.addEventListener('mousemove', this._boundHandlers.mousemove); + this.canvas.addEventListener('mousedown', this._boundHandlers.mousedown); + this.canvas.addEventListener('mouseup', this._boundHandlers.mouseup); + this.canvas.addEventListener('contextmenu', this._boundHandlers.contextmenu); + this.canvas.addEventListener('wheel', this._boundHandlers.wheel); + document.addEventListener('keydown', this._boundHandlers.keydown); + document.addEventListener('keyup', this._boundHandlers.keyup); + window.addEventListener('blur', this._boundHandlers.blur); + } + + _unbindEvents() { + this.canvas.removeEventListener('mousemove', this._boundHandlers.mousemove); + this.canvas.removeEventListener('mousedown', this._boundHandlers.mousedown); + this.canvas.removeEventListener('mouseup', this._boundHandlers.mouseup); + this.canvas.removeEventListener('contextmenu', this._boundHandlers.contextmenu); + this.canvas.removeEventListener('wheel', this._boundHandlers.wheel); + document.removeEventListener('keydown', this._boundHandlers.keydown); + document.removeEventListener('keyup', this._boundHandlers.keyup); + window.removeEventListener('blur', this._boundHandlers.blur); + } + + sendInputEvent(event) { + if (this.wsReady && this.ws && this.ws.isReady()) { + this.ws.getWebSocket().send(JSON.stringify(event)); + } + } + + _sendMouseMove(x, y) { + this.pendingMove = { x, y }; + + const now = Date.now(); + if (now - this.lastMoveTime >= this.MOVE_THROTTLE_MS) { + this._flushMouseMove(); + } else if (!this.moveThrottleTimer) { + this.moveThrottleTimer = setTimeout(() => { + this._flushMouseMove(); + this.moveThrottleTimer = null; + }, this.MOVE_THROTTLE_MS - (now - this.lastMoveTime)); + } + } + + _flushMouseMove() { + if (this.pendingMove) { + this.sendInputEvent({ + type: 'mouseMove', + x: this.pendingMove.x, + y: this.pendingMove.y + }); + this.lastMoveTime = Date.now(); + this.pendingMove = null; + } + } + + _handleMouseMove(e) { + const { x, y } = getScreenCoordinates(e.clientX, e.clientY, this.canvas, this.screenWidth, this.screenHeight); + this._sendMouseMove(x, y); + } + + _handleMouseDown(e) { + const button = e.button === 2 ? 'right' : 'left'; + this.sendInputEvent({ + type: 'mouseDown', + button: button + }); + } + + _handleMouseUp(e) { + const button = e.button === 2 ? 'right' : 'left'; + this.sendInputEvent({ + type: 'mouseUp', + button: button + }); + } + + _handleContextMenu(e) { + e.preventDefault(); + } + + _handleWheel(e) { + e.preventDefault(); + const delta = -Math.sign(e.deltaY) * 120; + this.sendInputEvent({ + type: 'mouseWheel', + delta: delta + }); + } + + _getKeyFromEvent(e) { + if (this.codeToKey[e.code]) { + return this.codeToKey[e.code]; + } + return e.key; + } + + _handleKeyDown(e) { + const key = this._getKeyFromEvent(e); + const keyId = key.toLowerCase(); + + if (!this.pressedKeys.has(keyId)) { + this.pressedKeys.add(keyId); + this.sendInputEvent({ + type: 'keyDown', + key: key + }); + } + + if (e.key === 'Tab' || e.key === ' ' || e.key.startsWith('Arrow')) { + e.preventDefault(); + } + } + + _handleKeyUp(e) { + const key = this._getKeyFromEvent(e); + const keyId = key.toLowerCase(); + + this.pressedKeys.delete(keyId); + this.sendInputEvent({ + type: 'keyUp', + key: key + }); + } + + _handleBlur() { + this.pressedKeys.forEach(keyId => { + this.sendInputEvent({ + type: 'keyUp', + key: keyId + }); + }); + this.pressedKeys.clear(); + } + + destroy() { + this._unbindEvents(); + if (this.moveThrottleTimer) { + clearTimeout(this.moveThrottleTimer); + } + if (this.ws) { + this.ws.close(); + } + } +} diff --git a/remote/public/js/jsmpeg.min.js b/remote/public/js/jsmpeg.min.js new file mode 100644 index 0000000..5c15a55 --- /dev/null +++ b/remote/public/js/jsmpeg.min.js @@ -0,0 +1 @@ +var JSMpeg={Player:null,VideoElement:null,BitBuffer:null,Source:{},Demuxer:{},Decoder:{},Renderer:{},AudioOutput:{},Now:function(){return window.performance?window.performance.now()/1e3:Date.now()/1e3},CreateVideoElements:function(){var elements=document.querySelectorAll(".jsmpeg");for(var i=0;i'+''+''+"";VideoElement.UNMUTE_BUTTON=''+''+''+''+''+""+"";return VideoElement}();JSMpeg.Player=function(){"use strict";var Player=function(url,options){this.options=options||{};if(options.source){this.source=new options.source(url,options);options.streaming=!!this.source.streaming}else if(url.match(/^wss?:\/\//)){this.source=new JSMpeg.Source.WebSocket(url,options);options.streaming=true}else if(options.progressive!==false){this.source=new JSMpeg.Source.AjaxProgressive(url,options);options.streaming=false}else{this.source=new JSMpeg.Source.Ajax(url,options);options.streaming=false}this.maxAudioLag=options.maxAudioLag||.25;this.loop=options.loop!==false;this.autoplay=!!options.autoplay||options.streaming;this.demuxer=new JSMpeg.Demuxer.TS(options);this.source.connect(this.demuxer);if(!options.disableWebAssembly&&JSMpeg.WASMModule.IsSupported()){this.wasmModule=JSMpeg.WASMModule.GetModule();options.wasmModule=this.wasmModule}if(options.video!==false){this.video=options.wasmModule?new JSMpeg.Decoder.MPEG1VideoWASM(options):new JSMpeg.Decoder.MPEG1Video(options);this.renderer=!options.disableGl&&JSMpeg.Renderer.WebGL.IsSupported()?new JSMpeg.Renderer.WebGL(options):new JSMpeg.Renderer.Canvas2D(options);this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.VIDEO_1,this.video);this.video.connect(this.renderer)}if(options.audio!==false&&JSMpeg.AudioOutput.WebAudio.IsSupported()){this.audio=options.wasmModule?new JSMpeg.Decoder.MP2AudioWASM(options):new JSMpeg.Decoder.MP2Audio(options);this.audioOut=new JSMpeg.AudioOutput.WebAudio(options);this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.AUDIO_1,this.audio);this.audio.connect(this.audioOut)}Object.defineProperty(this,"currentTime",{get:this.getCurrentTime,set:this.setCurrentTime});Object.defineProperty(this,"volume",{get:this.getVolume,set:this.setVolume});this.paused=true;this.unpauseOnShow=false;if(options.pauseWhenHidden!==false){document.addEventListener("visibilitychange",this.showHide.bind(this))}if(this.wasmModule){if(this.wasmModule.ready){this.startLoading()}else if(JSMpeg.WASM_BINARY_INLINED){var wasm=JSMpeg.Base64ToArrayBuffer(JSMpeg.WASM_BINARY_INLINED);this.wasmModule.loadFromBuffer(wasm,this.startLoading.bind(this))}else{this.wasmModule.loadFromFile("jsmpeg.wasm",this.startLoading.bind(this))}}else{this.startLoading()}};Player.prototype.startLoading=function(){this.source.start();if(this.autoplay){this.play()}};Player.prototype.showHide=function(ev){if(document.visibilityState==="hidden"){this.unpauseOnShow=this.wantsToPlay;this.pause()}else if(this.unpauseOnShow){this.play()}};Player.prototype.play=function(ev){if(this.animationId){return}this.animationId=requestAnimationFrame(this.update.bind(this));this.wantsToPlay=true;this.paused=false};Player.prototype.pause=function(ev){if(this.paused){return}cancelAnimationFrame(this.animationId);this.animationId=null;this.wantsToPlay=false;this.isPlaying=false;this.paused=true;if(this.audio&&this.audio.canPlay){this.audioOut.stop();this.seek(this.currentTime)}if(this.options.onPause){this.options.onPause(this)}};Player.prototype.getVolume=function(){return this.audioOut?this.audioOut.volume:0};Player.prototype.setVolume=function(volume){if(this.audioOut){this.audioOut.volume=volume}};Player.prototype.stop=function(ev){this.pause();this.seek(0);if(this.video&&this.options.decodeFirstFrame!==false){this.video.decode()}};Player.prototype.destroy=function(){this.pause();this.source.destroy();this.video&&this.video.destroy();this.renderer&&this.renderer.destroy();this.audio&&this.audio.destroy();this.audioOut&&this.audioOut.destroy()};Player.prototype.seek=function(time){var startOffset=this.audio&&this.audio.canPlay?this.audio.startTime:this.video.startTime;if(this.video){this.video.seek(time+startOffset)}if(this.audio){this.audio.seek(time+startOffset)}this.startTime=JSMpeg.Now()-time};Player.prototype.getCurrentTime=function(){return this.audio&&this.audio.canPlay?this.audio.currentTime-this.audio.startTime:this.video.currentTime-this.video.startTime};Player.prototype.setCurrentTime=function(time){this.seek(time)};Player.prototype.update=function(){this.animationId=requestAnimationFrame(this.update.bind(this));if(!this.source.established){if(this.renderer){this.renderer.renderProgress(this.source.progress)}return}if(!this.isPlaying){this.isPlaying=true;this.startTime=JSMpeg.Now()-this.currentTime;if(this.options.onPlay){this.options.onPlay(this)}}if(this.options.streaming){this.updateForStreaming()}else{this.updateForStaticFile()}};Player.prototype.updateForStreaming=function(){if(this.video){this.video.decode()}if(this.audio){var decoded=false;do{if(this.audioOut.enqueuedTime>this.maxAudioLag){this.audioOut.resetEnqueuedTime();this.audioOut.enabled=false}decoded=this.audio.decode()}while(decoded);this.audioOut.enabled=true}};Player.prototype.nextFrame=function(){if(this.source.established&&this.video){return this.video.decode()}return false};Player.prototype.updateForStaticFile=function(){var notEnoughData=false,headroom=0;if(this.audio&&this.audio.canPlay){while(!notEnoughData&&this.audio.decodedTime-this.audio.currentTime<.25){notEnoughData=!this.audio.decode()}if(this.video&&this.video.currentTime0){if(lateTime>frameTime*2){this.startTime+=lateTime}notEnoughData=!this.video.decode()}headroom=this.demuxer.currentTime-targetTime}this.source.resume(headroom);if(notEnoughData&&this.source.completed){if(this.loop){this.seek(0)}else{this.pause();if(this.options.onEnded){this.options.onEnded(this)}}}else if(notEnoughData&&this.options.onStalled){this.options.onStalled(this)}};return Player}();JSMpeg.BitBuffer=function(){"use strict";var BitBuffer=function(bufferOrLength,mode){if(typeof bufferOrLength==="object"){this.bytes=bufferOrLength instanceof Uint8Array?bufferOrLength:new Uint8Array(bufferOrLength);this.byteLength=this.bytes.length}else{this.bytes=new Uint8Array(bufferOrLength||1024*1024);this.byteLength=0}this.mode=mode||BitBuffer.MODE.EXPAND;this.index=0};BitBuffer.prototype.resize=function(size){var newBytes=new Uint8Array(size);if(this.byteLength!==0){this.byteLength=Math.min(this.byteLength,size);newBytes.set(this.bytes,0,this.byteLength)}this.bytes=newBytes;this.index=Math.min(this.index,this.byteLength<<3)};BitBuffer.prototype.evict=function(sizeNeeded){var bytePos=this.index>>3,available=this.bytes.length-this.byteLength;if(this.index===this.byteLength<<3||sizeNeeded>available+bytePos){this.byteLength=0;this.index=0;return}else if(bytePos===0){return}if(this.bytes.copyWithin){this.bytes.copyWithin(0,bytePos,this.byteLength)}else{this.bytes.set(this.bytes.subarray(bytePos,this.byteLength))}this.byteLength=this.byteLength-bytePos;this.index-=bytePos<<3;return};BitBuffer.prototype.write=function(buffers){var isArrayOfBuffers=typeof buffers[0]==="object",totalLength=0,available=this.bytes.length-this.byteLength;if(isArrayOfBuffers){var totalLength=0;for(var i=0;iavailable){if(this.mode===BitBuffer.MODE.EXPAND){var newSize=Math.max(this.bytes.length*2,totalLength-available);this.resize(newSize)}else{this.evict(totalLength)}}if(isArrayOfBuffers){for(var i=0;i>3;i>3;return i>=this.byteLength||this.bytes[i]==0&&this.bytes[i+1]==0&&this.bytes[i+2]==1};BitBuffer.prototype.peek=function(count){var offset=this.index;var value=0;while(count){var currentByte=this.bytes[offset>>3],remaining=8-(offset&7),read=remaining>8-read;value=value<>shift;offset+=read;count-=read}return value};BitBuffer.prototype.read=function(count){var value=this.peek(count);this.index+=count;return value};BitBuffer.prototype.skip=function(count){return this.index+=count};BitBuffer.prototype.rewind=function(count){this.index=Math.max(this.index-count,0)};BitBuffer.prototype.has=function(count){return(this.byteLength<<3)-this.index>=count};BitBuffer.MODE={EVICT:1,EXPAND:2};return BitBuffer}();JSMpeg.Source.Ajax=function(){"use strict";var AjaxSource=function(url,options){this.url=url;this.destination=null;this.request=null;this.streaming=false;this.completed=false;this.established=false;this.progress=0;this.onEstablishedCallback=options.onSourceEstablished;this.onCompletedCallback=options.onSourceCompleted};AjaxSource.prototype.connect=function(destination){this.destination=destination};AjaxSource.prototype.start=function(){this.request=new XMLHttpRequest;this.request.onreadystatechange=function(){if(this.request.readyState===this.request.DONE&&this.request.status===200){this.onLoad(this.request.response)}}.bind(this);this.request.onprogress=this.onProgress.bind(this);this.request.open("GET",this.url);this.request.responseType="arraybuffer";this.request.send()};AjaxSource.prototype.resume=function(secondsHeadroom){};AjaxSource.prototype.destroy=function(){this.request.abort()};AjaxSource.prototype.onProgress=function(ev){this.progress=ev.loaded/ev.total};AjaxSource.prototype.onLoad=function(data){this.established=true;this.completed=true;this.progress=1;if(this.onEstablishedCallback){this.onEstablishedCallback(this)}if(this.onCompletedCallback){this.onCompletedCallback(this)}if(this.destination){this.destination.write(data)}};return AjaxSource}();JSMpeg.Source.Fetch=function(){"use strict";var FetchSource=function(url,options){this.url=url;this.destination=null;this.request=null;this.streaming=true;this.completed=false;this.established=false;this.progress=0;this.aborted=false;this.onEstablishedCallback=options.onSourceEstablished;this.onCompletedCallback=options.onSourceCompleted};FetchSource.prototype.connect=function(destination){this.destination=destination};FetchSource.prototype.start=function(){var params={method:"GET",headers:new Headers,cache:"default"};self.fetch(this.url,params).then(function(res){if(res.ok&&(res.status>=200&&res.status<=299)){this.progress=1;this.established=true;return this.pump(res.body.getReader())}else{}}.bind(this)).catch(function(err){throw err})};FetchSource.prototype.pump=function(reader){return reader.read().then(function(result){if(result.done){this.completed=true}else{if(this.aborted){return reader.cancel()}if(this.destination){this.destination.write(result.value.buffer)}return this.pump(reader)}}.bind(this)).catch(function(err){throw err})};FetchSource.prototype.resume=function(secondsHeadroom){};FetchSource.prototype.abort=function(){this.aborted=true};return FetchSource}();JSMpeg.Source.AjaxProgressive=function(){"use strict";var AjaxProgressiveSource=function(url,options){this.url=url;this.destination=null;this.request=null;this.streaming=false;this.completed=false;this.established=false;this.progress=0;this.fileSize=0;this.loadedSize=0;this.chunkSize=options.chunkSize||1024*1024;this.isLoading=false;this.loadStartTime=0;this.throttled=options.throttled!==false;this.aborted=false;this.onEstablishedCallback=options.onSourceEstablished;this.onCompletedCallback=options.onSourceCompleted};AjaxProgressiveSource.prototype.connect=function(destination){this.destination=destination};AjaxProgressiveSource.prototype.start=function(){this.request=new XMLHttpRequest;this.request.onreadystatechange=function(){if(this.request.readyState===this.request.DONE){this.fileSize=parseInt(this.request.getResponseHeader("Content-Length"));this.loadNextChunk()}}.bind(this);this.request.onprogress=this.onProgress.bind(this);this.request.open("HEAD",this.url);this.request.send()};AjaxProgressiveSource.prototype.resume=function(secondsHeadroom){if(this.isLoading||!this.throttled){return}var worstCaseLoadingTime=this.loadTime*8+2;if(worstCaseLoadingTime>secondsHeadroom){this.loadNextChunk()}};AjaxProgressiveSource.prototype.destroy=function(){this.request.abort();this.aborted=true};AjaxProgressiveSource.prototype.loadNextChunk=function(){var start=this.loadedSize,end=Math.min(this.loadedSize+this.chunkSize-1,this.fileSize-1);if(start>=this.fileSize||this.aborted){this.completed=true;if(this.onCompletedCallback){this.onCompletedCallback(this)}return}this.isLoading=true;this.loadStartTime=JSMpeg.Now();this.request=new XMLHttpRequest;this.request.onreadystatechange=function(){if(this.request.readyState===this.request.DONE&&this.request.status>=200&&this.request.status<300){this.onChunkLoad(this.request.response)}else if(this.request.readyState===this.request.DONE){if(this.loadFails++<3){this.loadNextChunk()}}}.bind(this);if(start===0){this.request.onprogress=this.onProgress.bind(this)}this.request.open("GET",this.url+"?"+start+"-"+end);this.request.setRequestHeader("Range","bytes="+start+"-"+end);this.request.responseType="arraybuffer";this.request.send()};AjaxProgressiveSource.prototype.onProgress=function(ev){this.progress=ev.loaded/ev.total};AjaxProgressiveSource.prototype.onChunkLoad=function(data){var isFirstChunk=!this.established;this.established=true;this.progress=1;this.loadedSize+=data.byteLength;this.loadFails=0;this.isLoading=false;if(isFirstChunk&&this.onEstablishedCallback){this.onEstablishedCallback(this)}if(this.destination){this.destination.write(data)}this.loadTime=JSMpeg.Now()-this.loadStartTime;if(!this.throttled){this.loadNextChunk()}};return AjaxProgressiveSource}();JSMpeg.Source.WebSocket=function(){"use strict";var WSSource=function(url,options){this.url=url;this.options=options;this.socket=null;this.streaming=true;this.callbacks={connect:[],data:[]};this.destination=null;this.reconnectInterval=options.reconnectInterval!==undefined?options.reconnectInterval:5;this.shouldAttemptReconnect=!!this.reconnectInterval;this.completed=false;this.established=false;this.progress=0;this.reconnectTimeoutId=0;this.onEstablishedCallback=options.onSourceEstablished;this.onCompletedCallback=options.onSourceCompleted};WSSource.prototype.connect=function(destination){this.destination=destination};WSSource.prototype.destroy=function(){clearTimeout(this.reconnectTimeoutId);this.shouldAttemptReconnect=false;this.socket.close()};WSSource.prototype.start=function(){this.shouldAttemptReconnect=!!this.reconnectInterval;this.progress=0;this.established=false;if(this.options.protocols){this.socket=new WebSocket(this.url,this.options.protocols)}else{this.socket=new WebSocket(this.url)}this.socket.binaryType="arraybuffer";this.socket.onmessage=this.onMessage.bind(this);this.socket.onopen=this.onOpen.bind(this);this.socket.onerror=this.onClose.bind(this);this.socket.onclose=this.onClose.bind(this)};WSSource.prototype.resume=function(secondsHeadroom){};WSSource.prototype.onOpen=function(){this.progress=1};WSSource.prototype.onClose=function(){if(this.shouldAttemptReconnect){clearTimeout(this.reconnectTimeoutId);this.reconnectTimeoutId=setTimeout(function(){this.start()}.bind(this),this.reconnectInterval*1e3)}};WSSource.prototype.onMessage=function(ev){var isFirstChunk=!this.established;this.established=true;if(isFirstChunk&&this.onEstablishedCallback){this.onEstablishedCallback(this)}if(this.destination){this.destination.write(ev.data)}};return WSSource}();JSMpeg.Demuxer.TS=function(){"use strict";var TS=function(options){this.bits=null;this.leftoverBytes=null;this.guessVideoFrameEnd=true;this.pidsToStreamIds={};this.pesPacketInfo={};this.startTime=0;this.currentTime=0};TS.prototype.connect=function(streamId,destination){this.pesPacketInfo[streamId]={destination:destination,currentLength:0,totalLength:0,pts:0,buffers:[]}};TS.prototype.write=function(buffer){if(this.leftoverBytes){var totalLength=buffer.byteLength+this.leftoverBytes.byteLength;this.bits=new JSMpeg.BitBuffer(totalLength);this.bits.write([this.leftoverBytes,buffer])}else{this.bits=new JSMpeg.BitBuffer(buffer)}while(this.bits.has(188<<3)&&this.parsePacket()){}var leftoverCount=this.bits.byteLength-(this.bits.index>>3);this.leftoverBytes=leftoverCount>0?this.bits.bytes.subarray(this.bits.index>>3):null};TS.prototype.parsePacket=function(){if(this.bits.read(8)!==71){if(!this.resync()){return false}}var end=(this.bits.index>>3)+187;var transportError=this.bits.read(1),payloadStart=this.bits.read(1),transportPriority=this.bits.read(1),pid=this.bits.read(13),transportScrambling=this.bits.read(2),adaptationField=this.bits.read(2),continuityCounter=this.bits.read(4);var streamId=this.pidsToStreamIds[pid];if(payloadStart&&streamId){var pi=this.pesPacketInfo[streamId];if(pi&&pi.currentLength){this.packetComplete(pi)}}if(adaptationField&1){if(adaptationField&2){var adaptationFieldLength=this.bits.read(8);this.bits.skip(adaptationFieldLength<<3)}if(payloadStart&&this.bits.nextBytesAreStartCode()){this.bits.skip(24);streamId=this.bits.read(8);this.pidsToStreamIds[pid]=streamId;var packetLength=this.bits.read(16);this.bits.skip(8);var ptsDtsFlag=this.bits.read(2);this.bits.skip(6);var headerLength=this.bits.read(8);var payloadBeginIndex=this.bits.index+(headerLength<<3);var pi=this.pesPacketInfo[streamId];if(pi){var pts=0;if(ptsDtsFlag&2){this.bits.skip(4);var p32_30=this.bits.read(3);this.bits.skip(1);var p29_15=this.bits.read(15);this.bits.skip(1);var p14_0=this.bits.read(15);this.bits.skip(1);pts=(p32_30*1073741824+p29_15*32768+p14_0)/9e4;this.currentTime=pts;if(this.startTime===-1){this.startTime=pts}}var payloadLength=packetLength?packetLength-headerLength-3:0;this.packetStart(pi,pts,payloadLength)}this.bits.index=payloadBeginIndex}if(streamId){var pi=this.pesPacketInfo[streamId];if(pi){var start=this.bits.index>>3;var complete=this.packetAddData(pi,start,end);var hasPadding=!payloadStart&&adaptationField&2;if(complete||this.guessVideoFrameEnd&&hasPadding){this.packetComplete(pi)}}}}this.bits.index=end<<3;return true};TS.prototype.resync=function(){if(!this.bits.has(188*6<<3)){return false}var byteIndex=this.bits.index>>3;for(var i=0;i<187;i++){if(this.bits.bytes[byteIndex+i]===71){var foundSync=true;for(var j=1;j<5;j++){if(this.bits.bytes[byteIndex+i+188*j]!==71){foundSync=false;break}}if(foundSync){this.bits.index=byteIndex+i+1<<3;return true}}}console.warn("JSMpeg: Possible garbage data. Skipping.");this.bits.skip(187<<3);return false};TS.prototype.packetStart=function(pi,pts,payloadLength){pi.totalLength=payloadLength;pi.currentLength=0;pi.pts=pts};TS.prototype.packetAddData=function(pi,start,end){pi.buffers.push(this.bits.bytes.subarray(start,end));pi.currentLength+=end-start;var complete=pi.totalLength!==0&&pi.currentLength>=pi.totalLength;return complete};TS.prototype.packetComplete=function(pi){pi.destination.write(pi.pts,pi.buffers);pi.totalLength=0;pi.currentLength=0;pi.buffers=[]};TS.STREAM={PACK_HEADER:186,SYSTEM_HEADER:187,PROGRAM_MAP:188,PRIVATE_1:189,PADDING:190,PRIVATE_2:191,AUDIO_1:192,VIDEO_1:224,DIRECTORY:255};return TS}();JSMpeg.Decoder.Base=function(){"use strict";var BaseDecoder=function(options){this.destination=null;this.canPlay=false;this.collectTimestamps=!options.streaming;this.bytesWritten=0;this.timestamps=[];this.timestampIndex=0;this.startTime=0;this.decodedTime=0;Object.defineProperty(this,"currentTime",{get:this.getCurrentTime})};BaseDecoder.prototype.destroy=function(){};BaseDecoder.prototype.connect=function(destination){this.destination=destination};BaseDecoder.prototype.bufferGetIndex=function(){return this.bits.index};BaseDecoder.prototype.bufferSetIndex=function(index){this.bits.index=index};BaseDecoder.prototype.bufferWrite=function(buffers){return this.bits.write(buffers)};BaseDecoder.prototype.write=function(pts,buffers){if(this.collectTimestamps){if(this.timestamps.length===0){this.startTime=pts;this.decodedTime=pts}this.timestamps.push({index:this.bytesWritten<<3,time:pts})}this.bytesWritten+=this.bufferWrite(buffers);this.canPlay=true};BaseDecoder.prototype.seek=function(time){if(!this.collectTimestamps){return}this.timestampIndex=0;for(var i=0;itime){break}this.timestampIndex=i}var ts=this.timestamps[this.timestampIndex];if(ts){this.bufferSetIndex(ts.index);this.decodedTime=ts.time}else{this.bufferSetIndex(0);this.decodedTime=this.startTime}};BaseDecoder.prototype.decode=function(){this.advanceDecodedTime(0)};BaseDecoder.prototype.advanceDecodedTime=function(seconds){if(this.collectTimestamps){var newTimestampIndex=-1;var currentIndex=this.bufferGetIndex();for(var i=this.timestampIndex;icurrentIndex){break}newTimestampIndex=i}if(newTimestampIndex!==-1&&newTimestampIndex!==this.timestampIndex){this.timestampIndex=newTimestampIndex;this.decodedTime=this.timestamps[this.timestampIndex].time;return}}this.decodedTime+=seconds};BaseDecoder.prototype.getCurrentTime=function(){return this.decodedTime};return BaseDecoder}();JSMpeg.Decoder.MPEG1Video=function(){"use strict";var MPEG1=function(options){JSMpeg.Decoder.Base.call(this,options);this.onDecodeCallback=options.onVideoDecode;var bufferSize=options.videoBufferSize||512*1024;var bufferMode=options.streaming?JSMpeg.BitBuffer.MODE.EVICT:JSMpeg.BitBuffer.MODE.EXPAND;this.bits=new JSMpeg.BitBuffer(bufferSize,bufferMode);this.customIntraQuantMatrix=new Uint8Array(64);this.customNonIntraQuantMatrix=new Uint8Array(64);this.blockData=new Int32Array(64);this.currentFrame=0;this.decodeFirstFrame=options.decodeFirstFrame!==false};MPEG1.prototype=Object.create(JSMpeg.Decoder.Base.prototype);MPEG1.prototype.constructor=MPEG1;MPEG1.prototype.write=function(pts,buffers){JSMpeg.Decoder.Base.prototype.write.call(this,pts,buffers);if(!this.hasSequenceHeader){if(this.bits.findStartCode(MPEG1.START.SEQUENCE)===-1){return false}this.decodeSequenceHeader();if(this.decodeFirstFrame){this.decode()}}};MPEG1.prototype.decode=function(){var startTime=JSMpeg.Now();if(!this.hasSequenceHeader){return false}if(this.bits.findStartCode(MPEG1.START.PICTURE)===-1){var bufferedBytes=this.bits.byteLength-(this.bits.index>>3);return false}this.decodePicture();this.advanceDecodedTime(1/this.frameRate);var elapsedTime=JSMpeg.Now()-startTime;if(this.onDecodeCallback){this.onDecodeCallback(this,elapsedTime)}return true};MPEG1.prototype.readHuffman=function(codeTable){var state=0;do{state=codeTable[state+this.bits.read(1)]}while(state>=0&&codeTable[state]!==0);return codeTable[state+2]};MPEG1.prototype.frameRate=30;MPEG1.prototype.decodeSequenceHeader=function(){var newWidth=this.bits.read(12),newHeight=this.bits.read(12);this.bits.skip(4);this.frameRate=MPEG1.PICTURE_RATE[this.bits.read(4)];this.bits.skip(18+1+10+1);if(newWidth!==this.width||newHeight!==this.height){this.width=newWidth;this.height=newHeight;this.initBuffers();if(this.destination){this.destination.resize(newWidth,newHeight)}}if(this.bits.read(1)){for(var i=0;i<64;i++){this.customIntraQuantMatrix[MPEG1.ZIG_ZAG[i]]=this.bits.read(8)}this.intraQuantMatrix=this.customIntraQuantMatrix}if(this.bits.read(1)){for(var i=0;i<64;i++){var idx=MPEG1.ZIG_ZAG[i];this.customNonIntraQuantMatrix[idx]=this.bits.read(8)}this.nonIntraQuantMatrix=this.customNonIntraQuantMatrix}this.hasSequenceHeader=true};MPEG1.prototype.initBuffers=function(){this.intraQuantMatrix=MPEG1.DEFAULT_INTRA_QUANT_MATRIX;this.nonIntraQuantMatrix=MPEG1.DEFAULT_NON_INTRA_QUANT_MATRIX;this.mbWidth=this.width+15>>4;this.mbHeight=this.height+15>>4;this.mbSize=this.mbWidth*this.mbHeight;this.codedWidth=this.mbWidth<<4;this.codedHeight=this.mbHeight<<4;this.codedSize=this.codedWidth*this.codedHeight;this.halfWidth=this.mbWidth<<3;this.halfHeight=this.mbHeight<<3;this.currentY=new Uint8ClampedArray(this.codedSize);this.currentY32=new Uint32Array(this.currentY.buffer);this.currentCr=new Uint8ClampedArray(this.codedSize>>2);this.currentCr32=new Uint32Array(this.currentCr.buffer);this.currentCb=new Uint8ClampedArray(this.codedSize>>2);this.currentCb32=new Uint32Array(this.currentCb.buffer);this.forwardY=new Uint8ClampedArray(this.codedSize);this.forwardY32=new Uint32Array(this.forwardY.buffer);this.forwardCr=new Uint8ClampedArray(this.codedSize>>2);this.forwardCr32=new Uint32Array(this.forwardCr.buffer);this.forwardCb=new Uint8ClampedArray(this.codedSize>>2);this.forwardCb32=new Uint32Array(this.forwardCb.buffer)};MPEG1.prototype.currentY=null;MPEG1.prototype.currentCr=null;MPEG1.prototype.currentCb=null;MPEG1.prototype.pictureType=0;MPEG1.prototype.forwardY=null;MPEG1.prototype.forwardCr=null;MPEG1.prototype.forwardCb=null;MPEG1.prototype.fullPelForward=false;MPEG1.prototype.forwardFCode=0;MPEG1.prototype.forwardRSize=0;MPEG1.prototype.forwardF=0;MPEG1.prototype.decodePicture=function(skipOutput){this.currentFrame++;this.bits.skip(10);this.pictureType=this.bits.read(3);this.bits.skip(16);if(this.pictureType<=0||this.pictureType>=MPEG1.PICTURE_TYPE.B){return}if(this.pictureType===MPEG1.PICTURE_TYPE.PREDICTIVE){this.fullPelForward=this.bits.read(1);this.forwardFCode=this.bits.read(3);if(this.forwardFCode===0){return}this.forwardRSize=this.forwardFCode-1;this.forwardF=1<=MPEG1.START.SLICE_FIRST&&code<=MPEG1.START.SLICE_LAST){this.decodeSlice(code&255);code=this.bits.findNextStartCode()}if(code!==-1){this.bits.rewind(32)}if(this.destination){this.destination.render(this.currentY,this.currentCr,this.currentCb,true)}if(this.pictureType===MPEG1.PICTURE_TYPE.INTRA||this.pictureType===MPEG1.PICTURE_TYPE.PREDICTIVE){var tmpY=this.forwardY,tmpY32=this.forwardY32,tmpCr=this.forwardCr,tmpCr32=this.forwardCr32,tmpCb=this.forwardCb,tmpCb32=this.forwardCb32;this.forwardY=this.currentY;this.forwardY32=this.currentY32;this.forwardCr=this.currentCr;this.forwardCr32=this.currentCr32;this.forwardCb=this.currentCb;this.forwardCb32=this.currentCb32;this.currentY=tmpY;this.currentY32=tmpY32;this.currentCr=tmpCr;this.currentCr32=tmpCr32;this.currentCb=tmpCb;this.currentCb32=tmpCb32}};MPEG1.prototype.quantizerScale=0;MPEG1.prototype.sliceBegin=false;MPEG1.prototype.decodeSlice=function(slice){this.sliceBegin=true;this.macroblockAddress=(slice-1)*this.mbWidth-1;this.motionFwH=this.motionFwHPrev=0;this.motionFwV=this.motionFwVPrev=0;this.dcPredictorY=128;this.dcPredictorCr=128;this.dcPredictorCb=128;this.quantizerScale=this.bits.read(5);while(this.bits.read(1)){this.bits.skip(8)}do{this.decodeMacroblock()}while(!this.bits.nextBytesAreStartCode())};MPEG1.prototype.macroblockAddress=0;MPEG1.prototype.mbRow=0;MPEG1.prototype.mbCol=0;MPEG1.prototype.macroblockType=0;MPEG1.prototype.macroblockIntra=false;MPEG1.prototype.macroblockMotFw=false;MPEG1.prototype.motionFwH=0;MPEG1.prototype.motionFwV=0;MPEG1.prototype.motionFwHPrev=0;MPEG1.prototype.motionFwVPrev=0;MPEG1.prototype.decodeMacroblock=function(){var increment=0,t=this.readHuffman(MPEG1.MACROBLOCK_ADDRESS_INCREMENT);while(t===34){t=this.readHuffman(MPEG1.MACROBLOCK_ADDRESS_INCREMENT)}while(t===35){increment+=33;t=this.readHuffman(MPEG1.MACROBLOCK_ADDRESS_INCREMENT)}increment+=t;if(this.sliceBegin){this.sliceBegin=false;this.macroblockAddress+=increment}else{if(this.macroblockAddress+increment>=this.mbSize){return}if(increment>1){this.dcPredictorY=128;this.dcPredictorCr=128;this.dcPredictorCb=128;if(this.pictureType===MPEG1.PICTURE_TYPE.PREDICTIVE){this.motionFwH=this.motionFwHPrev=0;this.motionFwV=this.motionFwVPrev=0}}while(increment>1){this.macroblockAddress++;this.mbRow=this.macroblockAddress/this.mbWidth|0;this.mbCol=this.macroblockAddress%this.mbWidth;this.copyMacroblock(this.motionFwH,this.motionFwV,this.forwardY,this.forwardCr,this.forwardCb);increment--}this.macroblockAddress++}this.mbRow=this.macroblockAddress/this.mbWidth|0;this.mbCol=this.macroblockAddress%this.mbWidth;var mbTable=MPEG1.MACROBLOCK_TYPE[this.pictureType];this.macroblockType=this.readHuffman(mbTable);this.macroblockIntra=this.macroblockType&1;this.macroblockMotFw=this.macroblockType&8;if((this.macroblockType&16)!==0){this.quantizerScale=this.bits.read(5)}if(this.macroblockIntra){this.motionFwH=this.motionFwHPrev=0;this.motionFwV=this.motionFwVPrev=0}else{this.dcPredictorY=128;this.dcPredictorCr=128;this.dcPredictorCb=128;this.decodeMotionVectors();this.copyMacroblock(this.motionFwH,this.motionFwV,this.forwardY,this.forwardCr,this.forwardCb)}var cbp=(this.macroblockType&2)!==0?this.readHuffman(MPEG1.CODE_BLOCK_PATTERN):this.macroblockIntra?63:0;for(var block=0,mask=32;block<6;block++){if((cbp&mask)!==0){this.decodeBlock(block)}mask>>=1}};MPEG1.prototype.decodeMotionVectors=function(){var code,d,r=0;if(this.macroblockMotFw){code=this.readHuffman(MPEG1.MOTION);if(code!==0&&this.forwardF!==1){r=this.bits.read(this.forwardRSize);d=(Math.abs(code)-1<(this.forwardF<<4)-1){this.motionFwHPrev-=this.forwardF<<5}else if(this.motionFwHPrev<-this.forwardF<<4){this.motionFwHPrev+=this.forwardF<<5}this.motionFwH=this.motionFwHPrev;if(this.fullPelForward){this.motionFwH<<=1}code=this.readHuffman(MPEG1.MOTION);if(code!==0&&this.forwardF!==1){r=this.bits.read(this.forwardRSize);d=(Math.abs(code)-1<(this.forwardF<<4)-1){this.motionFwVPrev-=this.forwardF<<5}else if(this.motionFwVPrev<-this.forwardF<<4){this.motionFwVPrev+=this.forwardF<<5}this.motionFwV=this.motionFwVPrev;if(this.fullPelForward){this.motionFwV<<=1}}else if(this.pictureType===MPEG1.PICTURE_TYPE.PREDICTIVE){this.motionFwH=this.motionFwHPrev=0;this.motionFwV=this.motionFwVPrev=0}};MPEG1.prototype.copyMacroblock=function(motionH,motionV,sY,sCr,sCb){var width,scan,H,V,oddH,oddV,src,dest,last;var dY=this.currentY32,dCb=this.currentCb32,dCr=this.currentCr32;width=this.codedWidth;scan=width-16;H=motionH>>1;V=motionV>>1;oddH=(motionH&1)===1;oddV=(motionV&1)===1;src=((this.mbRow<<4)+V)*width+(this.mbCol<<4)+H;dest=this.mbRow*width+this.mbCol<<2;last=dest+(width<<2);var x,y1,y2,y;if(oddH){if(oddV){while(dest>2&255;y1=sY[src]+sY[src+width];src++;y|=y1+y2+2<<6&65280;y2=sY[src]+sY[src+width];src++;y|=y1+y2+2<<14&16711680;y1=sY[src]+sY[src+width];src++;y|=y1+y2+2<<22&4278190080;dY[dest++]=y}dest+=scan>>2;src+=scan-1}}else{while(dest>1&255;y1=sY[src++];y|=y1+y2+1<<7&65280;y2=sY[src++];y|=y1+y2+1<<15&16711680;y1=sY[src++];y|=y1+y2+1<<23&4278190080;dY[dest++]=y}dest+=scan>>2;src+=scan-1}}}else{if(oddV){while(dest>1&255;src++;y|=sY[src]+sY[src+width]+1<<7&65280;src++;y|=sY[src]+sY[src+width]+1<<15&16711680;src++;y|=sY[src]+sY[src+width]+1<<23&4278190080;src++;dY[dest++]=y}dest+=scan>>2;src+=scan}}else{while(dest>2;src+=scan}}}width=this.halfWidth;scan=width-8;H=motionH/2>>1;V=motionV/2>>1;oddH=(motionH/2&1)===1;oddV=(motionV/2&1)===1;src=((this.mbRow<<3)+V)*width+(this.mbCol<<3)+H;dest=this.mbRow*width+this.mbCol<<1;last=dest+(width<<1);var cr1,cr2,cr,cb1,cb2,cb;if(oddH){if(oddV){while(dest>2&255;cb=cb1+cb2+2>>2&255;cr1=sCr[src]+sCr[src+width];cb1=sCb[src]+sCb[src+width];src++;cr|=cr1+cr2+2<<6&65280;cb|=cb1+cb2+2<<6&65280;cr2=sCr[src]+sCr[src+width];cb2=sCb[src]+sCb[src+width];src++;cr|=cr1+cr2+2<<14&16711680;cb|=cb1+cb2+2<<14&16711680;cr1=sCr[src]+sCr[src+width];cb1=sCb[src]+sCb[src+width];src++;cr|=cr1+cr2+2<<22&4278190080;cb|=cb1+cb2+2<<22&4278190080;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan-1}}else{while(dest>1&255;cb=cb1+cb2+1>>1&255;cr1=sCr[src];cb1=sCb[src++];cr|=cr1+cr2+1<<7&65280;cb|=cb1+cb2+1<<7&65280;cr2=sCr[src];cb2=sCb[src++];cr|=cr1+cr2+1<<15&16711680;cb|=cb1+cb2+1<<15&16711680;cr1=sCr[src];cb1=sCb[src++];cr|=cr1+cr2+1<<23&4278190080;cb|=cb1+cb2+1<<23&4278190080;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan-1}}}else{if(oddV){while(dest>1&255;cb=sCb[src]+sCb[src+width]+1>>1&255;src++;cr|=sCr[src]+sCr[src+width]+1<<7&65280;cb|=sCb[src]+sCb[src+width]+1<<7&65280;src++;cr|=sCr[src]+sCr[src+width]+1<<15&16711680;cb|=sCb[src]+sCb[src+width]+1<<15&16711680;src++;cr|=sCr[src]+sCr[src+width]+1<<23&4278190080;cb|=sCb[src]+sCb[src+width]+1<<23&4278190080;src++;dCr[dest]=cr;dCb[dest]=cb;dest++}dest+=scan>>2;src+=scan}}else{while(dest>2;src+=scan}}}};MPEG1.prototype.dcPredictorY=0;MPEG1.prototype.dcPredictorCr=0;MPEG1.prototype.dcPredictorCb=0;MPEG1.prototype.blockData=null;MPEG1.prototype.decodeBlock=function(block){var n=0,quantMatrix;if(this.macroblockIntra){var predictor,dctSize;if(block<4){predictor=this.dcPredictorY;dctSize=this.readHuffman(MPEG1.DCT_DC_SIZE_LUMINANCE)}else{predictor=block===4?this.dcPredictorCr:this.dcPredictorCb;dctSize=this.readHuffman(MPEG1.DCT_DC_SIZE_CHROMINANCE)}if(dctSize>0){var differential=this.bits.read(dctSize);if((differential&1<0&&this.bits.read(1)===0){break}if(coeff===65535){run=this.bits.read(6);level=this.bits.read(8);if(level===0){level=this.bits.read(8)}else if(level===128){level=this.bits.read(8)-256}else if(level>128){level=level-256}}else{run=coeff>>8;level=coeff&255;if(this.bits.read(1)){level=-level}}n+=run;var dezigZagged=MPEG1.ZIG_ZAG[n];n++;level<<=1;if(!this.macroblockIntra){level+=level<0?-1:1}level=level*this.quantizerScale*quantMatrix[dezigZagged]>>4;if((level&1)===0){level-=level>0?1:-1}if(level>2047){level=2047}else if(level<-2048){level=-2048}this.blockData[dezigZagged]=level*MPEG1.PREMULTIPLIER_MATRIX[dezigZagged]}var destArray,destIndex,scan;if(block<4){destArray=this.currentY;scan=this.codedWidth-8;destIndex=this.mbRow*this.codedWidth+this.mbCol<<4;if((block&1)!==0){destIndex+=8}if((block&2)!==0){destIndex+=this.codedWidth<<3}}else{destArray=block===4?this.currentCb:this.currentCr;scan=(this.codedWidth>>1)-8;destIndex=(this.mbRow*this.codedWidth<<2)+(this.mbCol<<3)}if(this.macroblockIntra){if(n===1){MPEG1.CopyValueToDestination(this.blockData[0]+128>>8,destArray,destIndex,scan);this.blockData[0]=0}else{MPEG1.IDCT(this.blockData);MPEG1.CopyBlockToDestination(this.blockData,destArray,destIndex,scan);JSMpeg.Fill(this.blockData,0)}}else{if(n===1){MPEG1.AddValueToDestination(this.blockData[0]+128>>8,destArray,destIndex,scan);this.blockData[0]=0}else{MPEG1.IDCT(this.blockData);MPEG1.AddBlockToDestination(this.blockData,destArray,destIndex,scan);JSMpeg.Fill(this.blockData,0)}}n=0};MPEG1.CopyBlockToDestination=function(block,dest,index,scan){for(var n=0;n<64;n+=8,index+=scan+8){dest[index+0]=block[n+0];dest[index+1]=block[n+1];dest[index+2]=block[n+2];dest[index+3]=block[n+3];dest[index+4]=block[n+4];dest[index+5]=block[n+5];dest[index+6]=block[n+6];dest[index+7]=block[n+7]}};MPEG1.AddBlockToDestination=function(block,dest,index,scan){for(var n=0;n<64;n+=8,index+=scan+8){dest[index+0]+=block[n+0];dest[index+1]+=block[n+1];dest[index+2]+=block[n+2];dest[index+3]+=block[n+3];dest[index+4]+=block[n+4];dest[index+5]+=block[n+5];dest[index+6]+=block[n+6];dest[index+7]+=block[n+7]}};MPEG1.CopyValueToDestination=function(value,dest,index,scan){for(var n=0;n<64;n+=8,index+=scan+8){dest[index+0]=value;dest[index+1]=value;dest[index+2]=value;dest[index+3]=value;dest[index+4]=value;dest[index+5]=value;dest[index+6]=value;dest[index+7]=value}};MPEG1.AddValueToDestination=function(value,dest,index,scan){for(var n=0;n<64;n+=8,index+=scan+8){dest[index+0]+=value;dest[index+1]+=value;dest[index+2]+=value;dest[index+3]+=value;dest[index+4]+=value;dest[index+5]+=value;dest[index+6]+=value;dest[index+7]+=value}};MPEG1.IDCT=function(block){var b1,b3,b4,b6,b7,tmp1,tmp2,m0,x0,x1,x2,x3,x4,y3,y4,y5,y6,y7;for(var i=0;i<8;++i){b1=block[4*8+i];b3=block[2*8+i]+block[6*8+i];b4=block[5*8+i]-block[3*8+i];tmp1=block[1*8+i]+block[7*8+i];tmp2=block[3*8+i]+block[5*8+i];b6=block[1*8+i]-block[7*8+i];b7=tmp1+tmp2;m0=block[0*8+i];x4=(b6*473-b4*196+128>>8)-b7;x0=x4-((tmp1-tmp2)*362+128>>8);x1=m0-b1;x2=((block[2*8+i]-block[6*8+i])*362+128>>8)-b3;x3=m0+b1;y3=x1+x2;y4=x3+b3;y5=x1-x2;y6=x3-b3;y7=-x0-(b4*473+b6*196+128>>8);block[0*8+i]=b7+y4;block[1*8+i]=x4+y3;block[2*8+i]=y5-x0;block[3*8+i]=y6-y7;block[4*8+i]=y6+y7;block[5*8+i]=x0+y5;block[6*8+i]=y3-x4;block[7*8+i]=y4-b7}for(var i=0;i<64;i+=8){b1=block[4+i];b3=block[2+i]+block[6+i];b4=block[5+i]-block[3+i];tmp1=block[1+i]+block[7+i];tmp2=block[3+i]+block[5+i];b6=block[1+i]-block[7+i];b7=tmp1+tmp2;m0=block[0+i];x4=(b6*473-b4*196+128>>8)-b7;x0=x4-((tmp1-tmp2)*362+128>>8);x1=m0-b1;x2=((block[2+i]-block[6+i])*362+128>>8)-b3;x3=m0+b1;y3=x1+x2;y4=x3+b3;y5=x1-x2;y6=x3-b3;y7=-x0-(b4*473+b6*196+128>>8);block[0+i]=b7+y4+128>>8;block[1+i]=x4+y3+128>>8;block[2+i]=y5-x0+128>>8;block[3+i]=y6-y7+128>>8;block[4+i]=y6+y7+128>>8;block[5+i]=x0+y5+128>>8;block[6+i]=y3-x4+128>>8;block[7+i]=y4-b7+128>>8}};MPEG1.PICTURE_RATE=[0,23.976,24,25,29.97,30,50,59.94,60,0,0,0,0,0,0,0];MPEG1.ZIG_ZAG=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]);MPEG1.DEFAULT_INTRA_QUANT_MATRIX=new Uint8Array([8,16,19,22,26,27,29,34,16,16,22,24,27,29,34,37,19,22,26,27,29,34,34,38,22,22,26,27,29,34,37,40,22,26,27,29,32,35,40,48,26,27,29,32,35,40,48,58,26,27,29,34,38,46,56,69,27,29,35,38,46,56,69,83]);MPEG1.DEFAULT_NON_INTRA_QUANT_MATRIX=new Uint8Array([16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16]);MPEG1.PREMULTIPLIER_MATRIX=new Uint8Array([32,44,42,38,32,25,17,9,44,62,58,52,44,35,24,12,42,58,55,49,42,33,23,12,38,52,49,44,38,30,20,10,32,44,42,38,32,25,17,9,25,35,33,30,25,20,14,7,17,24,23,20,17,14,9,5,9,12,12,10,9,7,5,2]);MPEG1.MACROBLOCK_ADDRESS_INCREMENT=new Int16Array([1*3,2*3,0,3*3,4*3,0,0,0,1,5*3,6*3,0,7*3,8*3,0,9*3,10*3,0,11*3,12*3,0,0,0,3,0,0,2,13*3,14*3,0,15*3,16*3,0,0,0,5,0,0,4,17*3,18*3,0,19*3,20*3,0,0,0,7,0,0,6,21*3,22*3,0,23*3,24*3,0,25*3,26*3,0,27*3,28*3,0,-1,29*3,0,-1,30*3,0,31*3,32*3,0,33*3,34*3,0,35*3,36*3,0,37*3,38*3,0,0,0,9,0,0,8,39*3,40*3,0,41*3,42*3,0,43*3,44*3,0,45*3,46*3,0,0,0,15,0,0,14,0,0,13,0,0,12,0,0,11,0,0,10,47*3,-1,0,-1,48*3,0,49*3,50*3,0,51*3,52*3,0,53*3,54*3,0,55*3,56*3,0,57*3,58*3,0,59*3,60*3,0,61*3,-1,0,-1,62*3,0,63*3,64*3,0,65*3,66*3,0,67*3,68*3,0,69*3,70*3,0,71*3,72*3,0,73*3,74*3,0,0,0,21,0,0,20,0,0,19,0,0,18,0,0,17,0,0,16,0,0,35,0,0,34,0,0,33,0,0,32,0,0,31,0,0,30,0,0,29,0,0,28,0,0,27,0,0,26,0,0,25,0,0,24,0,0,23,0,0,22]);MPEG1.MACROBLOCK_TYPE_INTRA=new Int8Array([1*3,2*3,0,-1,3*3,0,0,0,1,0,0,17]);MPEG1.MACROBLOCK_TYPE_PREDICTIVE=new Int8Array([1*3,2*3,0,3*3,4*3,0,0,0,10,5*3,6*3,0,0,0,2,7*3,8*3,0,0,0,8,9*3,10*3,0,11*3,12*3,0,-1,13*3,0,0,0,18,0,0,26,0,0,1,0,0,17]);MPEG1.MACROBLOCK_TYPE_B=new Int8Array([1*3,2*3,0,3*3,5*3,0,4*3,6*3,0,8*3,7*3,0,0,0,12,9*3,10*3,0,0,0,14,13*3,14*3,0,12*3,11*3,0,0,0,4,0,0,6,18*3,16*3,0,15*3,17*3,0,0,0,8,0,0,10,-1,19*3,0,0,0,1,20*3,21*3,0,0,0,30,0,0,17,0,0,22,0,0,26]);MPEG1.MACROBLOCK_TYPE=[null,MPEG1.MACROBLOCK_TYPE_INTRA,MPEG1.MACROBLOCK_TYPE_PREDICTIVE,MPEG1.MACROBLOCK_TYPE_B];MPEG1.CODE_BLOCK_PATTERN=new Int16Array([2*3,1*3,0,3*3,6*3,0,4*3,5*3,0,8*3,11*3,0,12*3,13*3,0,9*3,7*3,0,10*3,14*3,0,20*3,19*3,0,18*3,16*3,0,23*3,17*3,0,27*3,25*3,0,21*3,28*3,0,15*3,22*3,0,24*3,26*3,0,0,0,60,35*3,40*3,0,44*3,48*3,0,38*3,36*3,0,42*3,47*3,0,29*3,31*3,0,39*3,32*3,0,0,0,32,45*3,46*3,0,33*3,41*3,0,43*3,34*3,0,0,0,4,30*3,37*3,0,0,0,8,0,0,16,0,0,44,50*3,56*3,0,0,0,28,0,0,52,0,0,62,61*3,59*3,0,52*3,60*3,0,0,0,1,55*3,54*3,0,0,0,61,0,0,56,57*3,58*3,0,0,0,2,0,0,40,51*3,62*3,0,0,0,48,64*3,63*3,0,49*3,53*3,0,0,0,20,0,0,12,80*3,83*3,0,0,0,63,77*3,75*3,0,65*3,73*3,0,84*3,66*3,0,0,0,24,0,0,36,0,0,3,69*3,87*3,0,81*3,79*3,0,68*3,71*3,0,70*3,78*3,0,67*3,76*3,0,72*3,74*3,0,86*3,85*3,0,88*3,82*3,0,-1,94*3,0,95*3,97*3,0,0,0,33,0,0,9,106*3,110*3,0,102*3,116*3,0,0,0,5,0,0,10,93*3,89*3,0,0,0,6,0,0,18,0,0,17,0,0,34,113*3,119*3,0,103*3,104*3,0,90*3,92*3,0,109*3,107*3,0,117*3,118*3,0,101*3,99*3,0,98*3,96*3,0,100*3,91*3,0,114*3,115*3,0,105*3,108*3,0,112*3,111*3,0,121*3,125*3,0,0,0,41,0,0,14,0,0,21,124*3,122*3,0,120*3,123*3,0,0,0,11,0,0,19,0,0,7,0,0,35,0,0,13,0,0,50,0,0,49,0,0,58,0,0,37,0,0,25,0,0,45,0,0,57,0,0,26,0,0,29,0,0,38,0,0,53,0,0,23,0,0,43,0,0,46,0,0,42,0,0,22,0,0,54,0,0,51,0,0,15,0,0,30,0,0,39,0,0,47,0,0,55,0,0,27,0,0,59,0,0,31]);MPEG1.MOTION=new Int16Array([1*3,2*3,0,4*3,3*3,0,0,0,0,6*3,5*3,0,8*3,7*3,0,0,0,-1,0,0,1,9*3,10*3,0,12*3,11*3,0,0,0,2,0,0,-2,14*3,15*3,0,16*3,13*3,0,20*3,18*3,0,0,0,3,0,0,-3,17*3,19*3,0,-1,23*3,0,27*3,25*3,0,26*3,21*3,0,24*3,22*3,0,32*3,28*3,0,29*3,31*3,0,-1,33*3,0,36*3,35*3,0,0,0,-4,30*3,34*3,0,0,0,4,0,0,-7,0,0,5,37*3,41*3,0,0,0,-5,0,0,7,38*3,40*3,0,42*3,39*3,0,0,0,-6,0,0,6,51*3,54*3,0,50*3,49*3,0,45*3,46*3,0,52*3,47*3,0,43*3,53*3,0,44*3,48*3,0,0,0,10,0,0,9,0,0,8,0,0,-8,57*3,66*3,0,0,0,-9,60*3,64*3,0,56*3,61*3,0,55*3,62*3,0,58*3,63*3,0,0,0,-10,59*3,65*3,0,0,0,12,0,0,16,0,0,13,0,0,14,0,0,11,0,0,15,0,0,-16,0,0,-12,0,0,-14,0,0,-15,0,0,-11,0,0,-13]);MPEG1.DCT_DC_SIZE_LUMINANCE=new Int8Array([2*3,1*3,0,6*3,5*3,0,3*3,4*3,0,0,0,1,0,0,2,9*3,8*3,0,7*3,10*3,0,0,0,0,12*3,11*3,0,0,0,4,0,0,3,13*3,14*3,0,0,0,5,0,0,6,16*3,15*3,0,17*3,-1,0,0,0,7,0,0,8]);MPEG1.DCT_DC_SIZE_CHROMINANCE=new Int8Array([2*3,1*3,0,4*3,3*3,0,6*3,5*3,0,8*3,7*3,0,0,0,2,0,0,1,0,0,0,10*3,9*3,0,0,0,3,12*3,11*3,0,0,0,4,14*3,13*3,0,0,0,5,16*3,15*3,0,0,0,6,17*3,-1,0,0,0,7,0,0,8]);MPEG1.DCT_COEFF=new Int32Array([1*3,2*3,0,4*3,3*3,0,0,0,1,7*3,8*3,0,6*3,5*3,0,13*3,9*3,0,11*3,10*3,0,14*3,12*3,0,0,0,257,20*3,22*3,0,18*3,21*3,0,16*3,19*3,0,0,0,513,17*3,15*3,0,0,0,2,0,0,3,27*3,25*3,0,29*3,31*3,0,24*3,26*3,0,32*3,30*3,0,0,0,1025,23*3,28*3,0,0,0,769,0,0,258,0,0,1793,0,0,65535,0,0,1537,37*3,36*3,0,0,0,1281,35*3,34*3,0,39*3,38*3,0,33*3,42*3,0,40*3,41*3,0,52*3,50*3,0,54*3,53*3,0,48*3,49*3,0,43*3,45*3,0,46*3,44*3,0,0,0,2049,0,0,4,0,0,514,0,0,2305,51*3,47*3,0,55*3,57*3,0,60*3,56*3,0,59*3,58*3,0,61*3,62*3,0,0,0,2561,0,0,3329,0,0,6,0,0,259,0,0,5,0,0,770,0,0,2817,0,0,3073,76*3,75*3,0,67*3,70*3,0,73*3,71*3,0,78*3,74*3,0,72*3,77*3,0,69*3,64*3,0,68*3,63*3,0,66*3,65*3,0,81*3,87*3,0,91*3,80*3,0,82*3,79*3,0,83*3,86*3,0,93*3,92*3,0,84*3,85*3,0,90*3,94*3,0,88*3,89*3,0,0,0,515,0,0,260,0,0,7,0,0,1026,0,0,1282,0,0,4097,0,0,3841,0,0,3585,105*3,107*3,0,111*3,114*3,0,104*3,97*3,0,125*3,119*3,0,96*3,98*3,0,-1,123*3,0,95*3,101*3,0,106*3,121*3,0,99*3,102*3,0,113*3,103*3,0,112*3,116*3,0,110*3,100*3,0,124*3,115*3,0,117*3,122*3,0,109*3,118*3,0,120*3,108*3,0,127*3,136*3,0,139*3,140*3,0,130*3,126*3,0,145*3,146*3,0,128*3,129*3,0,0,0,2050,132*3,134*3,0,155*3,154*3,0,0,0,8,137*3,133*3,0,143*3,144*3,0,151*3,138*3,0,142*3,141*3,0,0,0,10,0,0,9,0,0,11,0,0,5377,0,0,1538,0,0,771,0,0,5121,0,0,1794,0,0,4353,0,0,4609,0,0,4865,148*3,152*3,0,0,0,1027,153*3,150*3,0,0,0,261,131*3,135*3,0,0,0,516,149*3,147*3,0,172*3,173*3,0,162*3,158*3,0,170*3,161*3,0,168*3,166*3,0,157*3,179*3,0,169*3,167*3,0,174*3,171*3,0,178*3,177*3,0,156*3,159*3,0,164*3,165*3,0,183*3,182*3,0,175*3,176*3,0,0,0,263,0,0,2562,0,0,2306,0,0,5633,0,0,5889,0,0,6401,0,0,6145,0,0,1283,0,0,772,0,0,13,0,0,12,0,0,14,0,0,15,0,0,517,0,0,6657,0,0,262,180*3,181*3,0,160*3,163*3,0,196*3,199*3,0,0,0,27,203*3,185*3,0,202*3,201*3,0,0,0,19,0,0,22,197*3,207*3,0,0,0,18,191*3,192*3,0,188*3,190*3,0,0,0,20,184*3,194*3,0,0,0,21,186*3,193*3,0,0,0,23,204*3,198*3,0,0,0,25,0,0,24,200*3,205*3,0,0,0,31,0,0,30,0,0,28,0,0,29,0,0,26,0,0,17,0,0,16,189*3,206*3,0,187*3,195*3,0,218*3,211*3,0,0,0,37,215*3,216*3,0,0,0,36,210*3,212*3,0,0,0,34,213*3,209*3,0,221*3,222*3,0,219*3,208*3,0,217*3,214*3,0,223*3,220*3,0,0,0,35,0,0,267,0,0,40,0,0,268,0,0,266,0,0,32,0,0,264,0,0,265,0,0,38,0,0,269,0,0,270,0,0,33,0,0,39,0,0,7937,0,0,6913,0,0,7681,0,0,4098,0,0,7425,0,0,7169,0,0,271,0,0,274,0,0,273,0,0,272,0,0,1539,0,0,2818,0,0,3586,0,0,3330,0,0,3074,0,0,3842]);MPEG1.PICTURE_TYPE={INTRA:1,PREDICTIVE:2,B:3};MPEG1.START={SEQUENCE:179,SLICE_FIRST:1,SLICE_LAST:175,PICTURE:0,EXTENSION:181,USER_DATA:178};return MPEG1}();JSMpeg.Decoder.MPEG1VideoWASM=function(){"use strict";var MPEG1WASM=function(options){JSMpeg.Decoder.Base.call(this,options);this.onDecodeCallback=options.onVideoDecode;this.module=options.wasmModule;this.bufferSize=options.videoBufferSize||512*1024;this.bufferMode=options.streaming?JSMpeg.BitBuffer.MODE.EVICT:JSMpeg.BitBuffer.MODE.EXPAND;this.decodeFirstFrame=options.decodeFirstFrame!==false;this.hasSequenceHeader=false};MPEG1WASM.prototype=Object.create(JSMpeg.Decoder.Base.prototype);MPEG1WASM.prototype.constructor=MPEG1WASM;MPEG1WASM.prototype.initializeWasmDecoder=function(){if(!this.module.instance){console.warn("JSMpeg: WASM module not compiled yet");return}this.instance=this.module.instance;this.functions=this.module.instance.exports;this.decoder=this.functions._mpeg1_decoder_create(this.bufferSize,this.bufferMode)};MPEG1WASM.prototype.destroy=function(){if(!this.decoder){return}this.functions._mpeg1_decoder_destroy(this.decoder)};MPEG1WASM.prototype.bufferGetIndex=function(){if(!this.decoder){return}return this.functions._mpeg1_decoder_get_index(this.decoder)};MPEG1WASM.prototype.bufferSetIndex=function(index){if(!this.decoder){return}this.functions._mpeg1_decoder_set_index(this.decoder,index)};MPEG1WASM.prototype.bufferWrite=function(buffers){if(!this.decoder){this.initializeWasmDecoder()}var totalLength=0;for(var i=0;i>2));var dcb=this.instance.heapU8.subarray(ptrCb,ptrCb+(this.codedSize>>2));this.destination.render(dy,dcr,dcb,false)}this.advanceDecodedTime(1/this.frameRate);var elapsedTime=JSMpeg.Now()-startTime;if(this.onDecodeCallback){this.onDecodeCallback(this,elapsedTime)}return true};return MPEG1WASM}();JSMpeg.Decoder.MP2Audio=function(){"use strict";var MP2=function(options){JSMpeg.Decoder.Base.call(this,options);this.onDecodeCallback=options.onAudioDecode;var bufferSize=options.audioBufferSize||128*1024;var bufferMode=options.streaming?JSMpeg.BitBuffer.MODE.EVICT:JSMpeg.BitBuffer.MODE.EXPAND;this.bits=new JSMpeg.BitBuffer(bufferSize,bufferMode);this.left=new Float32Array(1152);this.right=new Float32Array(1152);this.sampleRate=44100;this.D=new Float32Array(1024);this.D.set(MP2.SYNTHESIS_WINDOW,0);this.D.set(MP2.SYNTHESIS_WINDOW,512);this.V=[new Float32Array(1024),new Float32Array(1024)];this.U=new Int32Array(32);this.VPos=0;this.allocation=[new Array(32),new Array(32)];this.scaleFactorInfo=[new Uint8Array(32),new Uint8Array(32)];this.scaleFactor=[new Array(32),new Array(32)];this.sample=[new Array(32),new Array(32)];for(var j=0;j<2;j++){for(var i=0;i<32;i++){this.scaleFactor[j][i]=[0,0,0];this.sample[j][i]=[0,0,0]}}};MP2.prototype=Object.create(JSMpeg.Decoder.Base.prototype);MP2.prototype.constructor=MP2;MP2.prototype.decode=function(){var startTime=JSMpeg.Now();var pos=this.bits.index>>3;if(pos>=this.bits.byteLength){return false}var decoded=this.decodeFrame(this.left,this.right);this.bits.index=pos+decoded<<3;if(!decoded){return false}if(this.destination){this.destination.play(this.sampleRate,this.left,this.right)}this.advanceDecodedTime(this.left.length/this.sampleRate);var elapsedTime=JSMpeg.Now()-startTime;if(this.onDecodeCallback){this.onDecodeCallback(this,elapsedTime)}return true};MP2.prototype.getCurrentTime=function(){var enqueuedTime=this.destination?this.destination.enqueuedTime:0;return this.decodedTime-enqueuedTime};MP2.prototype.decodeFrame=function(left,right){var sync=this.bits.read(11),version=this.bits.read(2),layer=this.bits.read(2),hasCRC=!this.bits.read(1);if(sync!==MP2.FRAME_SYNC||version!==MP2.VERSION.MPEG_1||layer!==MP2.LAYER.II){return 0}var bitrateIndex=this.bits.read(4)-1;if(bitrateIndex>13){return 0}var sampleRateIndex=this.bits.read(2);var sampleRate=MP2.SAMPLE_RATE[sampleRateIndex];if(sampleRateIndex===3){return 0}if(version===MP2.VERSION.MPEG_2){sampleRateIndex+=4;bitrateIndex+=14}var padding=this.bits.read(1),privat=this.bits.read(1),mode=this.bits.read(2);var bound=0;if(mode===MP2.MODE.JOINT_STEREO){bound=this.bits.read(2)+1<<2}else{this.bits.skip(2);bound=mode===MP2.MODE.MONO?0:32}this.bits.skip(4);if(hasCRC){this.bits.skip(16)}var bitrate=MP2.BIT_RATE[bitrateIndex],sampleRate=MP2.SAMPLE_RATE[sampleRateIndex],frameSize=144e3*bitrate/sampleRate+padding|0;var tab3=0;var sblimit=0;if(version===MP2.VERSION.MPEG_2){tab3=2;sblimit=30}else{var tab1=mode===MP2.MODE.MONO?0:1;var tab2=MP2.QUANT_LUT_STEP_1[tab1][bitrateIndex];tab3=MP2.QUANT_LUT_STEP_2[tab2][sampleRateIndex];sblimit=tab3&63;tab3>>=6}if(bound>sblimit){bound=sblimit}for(var sb=0;sb>1);var vIndex=this.VPos%128>>1;while(vIndex<1024){for(var i=0;i<32;++i){this.U[i]+=this.D[dIndex++]*this.V[ch][vIndex++]}vIndex+=128-32;dIndex+=64-32}vIndex=128-32+1024-vIndex;dIndex-=512-32;while(vIndex<1024){for(var i=0;i<32;++i){this.U[i]+=this.D[dIndex++]*this.V[ch][vIndex++]}vIndex+=128-32;dIndex+=64-32}var outChannel=ch===0?left:right;for(var j=0;j<32;j++){outChannel[outPos+j]=this.U[j]/2147418112}}outPos+=32}}}this.sampleRate=sampleRate;return frameSize};MP2.prototype.readAllocation=function(sb,tab3){var tab4=MP2.QUANT_LUT_STEP_3[tab3][sb];var qtab=MP2.QUANT_LUT_STEP4[tab4&15][this.bits.read(tab4>>4)];return qtab?MP2.QUANT_TAB[qtab-1]:0};MP2.prototype.readSamples=function(ch,sb,part){var q=this.allocation[ch][sb],sf=this.scaleFactor[ch][sb][part],sample=this.sample[ch][sb],val=0;if(!q){sample[0]=sample[1]=sample[2]=0;return}if(sf===63){sf=0}else{var shift=sf/3|0;sf=MP2.SCALEFACTOR_BASE[sf%3]+(1<>1)>>shift}var adj=q.levels;if(q.group){val=this.bits.read(q.bits);sample[0]=val%adj;val=val/adj|0;sample[1]=val%adj;sample[2]=val/adj|0}else{sample[0]=this.bits.read(q.bits);sample[1]=this.bits.read(q.bits);sample[2]=this.bits.read(q.bits)}var scale=65536/(adj+1)|0;adj=(adj+1>>1)-1;val=(adj-sample[0])*scale;sample[0]=val*(sf>>12)+(val*(sf&4095)+2048>>12)>>12;val=(adj-sample[1])*scale;sample[1]=val*(sf>>12)+(val*(sf&4095)+2048>>12)>>12;val=(adj-sample[2])*scale;sample[2]=val*(sf>>12)+(val*(sf&4095)+2048>>12)>>12};MP2.MatrixTransform=function(s,ss,d,dp){var t01,t02,t03,t04,t05,t06,t07,t08,t09,t10,t11,t12,t13,t14,t15,t16,t17,t18,t19,t20,t21,t22,t23,t24,t25,t26,t27,t28,t29,t30,t31,t32,t33;t01=s[0][ss]+s[31][ss];t02=(s[0][ss]-s[31][ss])*.500602998235;t03=s[1][ss]+s[30][ss];t04=(s[1][ss]-s[30][ss])*.505470959898;t05=s[2][ss]+s[29][ss];t06=(s[2][ss]-s[29][ss])*.515447309923;t07=s[3][ss]+s[28][ss];t08=(s[3][ss]-s[28][ss])*.53104259109;t09=s[4][ss]+s[27][ss];t10=(s[4][ss]-s[27][ss])*.553103896034;t11=s[5][ss]+s[26][ss];t12=(s[5][ss]-s[26][ss])*.582934968206;t13=s[6][ss]+s[25][ss];t14=(s[6][ss]-s[25][ss])*.622504123036;t15=s[7][ss]+s[24][ss];t16=(s[7][ss]-s[24][ss])*.674808341455;t17=s[8][ss]+s[23][ss];t18=(s[8][ss]-s[23][ss])*.744536271002;t19=s[9][ss]+s[22][ss];t20=(s[9][ss]-s[22][ss])*.839349645416;t21=s[10][ss]+s[21][ss];t22=(s[10][ss]-s[21][ss])*.972568237862;t23=s[11][ss]+s[20][ss];t24=(s[11][ss]-s[20][ss])*1.16943993343;t25=s[12][ss]+s[19][ss];t26=(s[12][ss]-s[19][ss])*1.48416461631;t27=s[13][ss]+s[18][ss];t28=(s[13][ss]-s[18][ss])*2.05778100995;t29=s[14][ss]+s[17][ss];t30=(s[14][ss]-s[17][ss])*3.40760841847;t31=s[15][ss]+s[16][ss];t32=(s[15][ss]-s[16][ss])*10.1900081235;t33=t01+t31;t31=(t01-t31)*.502419286188;t01=t03+t29;t29=(t03-t29)*.52249861494;t03=t05+t27;t27=(t05-t27)*.566944034816;t05=t07+t25;t25=(t07-t25)*.64682178336;t07=t09+t23;t23=(t09-t23)*.788154623451;t09=t11+t21;t21=(t11-t21)*1.06067768599;t11=t13+t19;t19=(t13-t19)*1.72244709824;t13=t15+t17;t17=(t15-t17)*5.10114861869;t15=t33+t13;t13=(t33-t13)*.509795579104;t33=t01+t11;t01=(t01-t11)*.601344886935;t11=t03+t09;t09=(t03-t09)*.899976223136;t03=t05+t07;t07=(t05-t07)*2.56291544774;t05=t15+t03;t15=(t15-t03)*.541196100146;t03=t33+t11;t11=(t33-t11)*1.30656296488;t33=t05+t03;t05=(t05-t03)*.707106781187;t03=t15+t11;t15=(t15-t11)*.707106781187;t03+=t15;t11=t13+t07;t13=(t13-t07)*.541196100146;t07=t01+t09;t09=(t01-t09)*1.30656296488;t01=t11+t07;t07=(t11-t07)*.707106781187;t11=t13+t09;t13=(t13-t09)*.707106781187;t11+=t13;t01+=t11;t11+=t07;t07+=t13;t09=t31+t17;t31=(t31-t17)*.509795579104;t17=t29+t19;t29=(t29-t19)*.601344886935;t19=t27+t21;t21=(t27-t21)*.899976223136;t27=t25+t23;t23=(t25-t23)*2.56291544774;t25=t09+t27;t09=(t09-t27)*.541196100146;t27=t17+t19;t19=(t17-t19)*1.30656296488;t17=t25+t27;t27=(t25-t27)*.707106781187;t25=t09+t19;t19=(t09-t19)*.707106781187;t25+=t19;t09=t31+t23;t31=(t31-t23)*.541196100146;t23=t29+t21;t21=(t29-t21)*1.30656296488;t29=t09+t23;t23=(t09-t23)*.707106781187;t09=t31+t21;t31=(t31-t21)*.707106781187;t09+=t31;t29+=t09;t09+=t23;t23+=t31;t17+=t29;t29+=t25;t25+=t09;t09+=t27;t27+=t23;t23+=t19;t19+=t31;t21=t02+t32;t02=(t02-t32)*.502419286188;t32=t04+t30;t04=(t04-t30)*.52249861494;t30=t06+t28;t28=(t06-t28)*.566944034816;t06=t08+t26;t08=(t08-t26)*.64682178336;t26=t10+t24;t10=(t10-t24)*.788154623451;t24=t12+t22;t22=(t12-t22)*1.06067768599;t12=t14+t20;t20=(t14-t20)*1.72244709824;t14=t16+t18;t16=(t16-t18)*5.10114861869;t18=t21+t14;t14=(t21-t14)*.509795579104;t21=t32+t12;t32=(t32-t12)*.601344886935;t12=t30+t24;t24=(t30-t24)*.899976223136;t30=t06+t26;t26=(t06-t26)*2.56291544774;t06=t18+t30;t18=(t18-t30)*.541196100146;t30=t21+t12;t12=(t21-t12)*1.30656296488;t21=t06+t30;t30=(t06-t30)*.707106781187;t06=t18+t12;t12=(t18-t12)*.707106781187;t06+=t12;t18=t14+t26;t26=(t14-t26)*.541196100146;t14=t32+t24;t24=(t32-t24)*1.30656296488;t32=t18+t14;t14=(t18-t14)*.707106781187;t18=t26+t24;t24=(t26-t24)*.707106781187;t18+=t24;t32+=t18;t18+=t14;t26=t14+t24;t14=t02+t16;t02=(t02-t16)*.509795579104;t16=t04+t20;t04=(t04-t20)*.601344886935;t20=t28+t22;t22=(t28-t22)*.899976223136;t28=t08+t10;t10=(t08-t10)*2.56291544774;t08=t14+t28;t14=(t14-t28)*.541196100146;t28=t16+t20;t20=(t16-t20)*1.30656296488;t16=t08+t28;t28=(t08-t28)*.707106781187;t08=t14+t20;t20=(t14-t20)*.707106781187;t08+=t20;t14=t02+t10;t02=(t02-t10)*.541196100146;t10=t04+t22;t22=(t04-t22)*1.30656296488;t04=t14+t10;t10=(t14-t10)*.707106781187;t14=t02+t22;t02=(t02-t22)*.707106781187;t14+=t02;t04+=t14;t14+=t10;t10+=t02;t16+=t04;t04+=t08;t08+=t14;t14+=t28;t28+=t10;t10+=t20;t20+=t02;t21+=t16;t16+=t32;t32+=t04;t04+=t06;t06+=t08;t08+=t18;t18+=t14;t14+=t30;t30+=t28;t28+=t26;t26+=t10;t10+=t12;t12+=t20;t20+=t24;t24+=t02;d[dp+48]=-t33;d[dp+49]=d[dp+47]=-t21;d[dp+50]=d[dp+46]=-t17;d[dp+51]=d[dp+45]=-t16;d[dp+52]=d[dp+44]=-t01;d[dp+53]=d[dp+43]=-t32;d[dp+54]=d[dp+42]=-t29;d[dp+55]=d[dp+41]=-t04;d[dp+56]=d[dp+40]=-t03;d[dp+57]=d[dp+39]=-t06;d[dp+58]=d[dp+38]=-t25;d[dp+59]=d[dp+37]=-t08;d[dp+60]=d[dp+36]=-t11;d[dp+61]=d[dp+35]=-t18;d[dp+62]=d[dp+34]=-t09;d[dp+63]=d[dp+33]=-t14;d[dp+32]=-t05;d[dp+0]=t05;d[dp+31]=-t30;d[dp+1]=t30;d[dp+30]=-t27;d[dp+2]=t27;d[dp+29]=-t28;d[dp+3]=t28;d[dp+28]=-t07;d[dp+4]=t07;d[dp+27]=-t26;d[dp+5]=t26;d[dp+26]=-t23;d[dp+6]=t23;d[dp+25]=-t10;d[dp+7]=t10;d[dp+24]=-t15;d[dp+8]=t15;d[dp+23]=-t12;d[dp+9]=t12;d[dp+22]=-t19;d[dp+10]=t19;d[dp+21]=-t20;d[dp+11]=t20;d[dp+20]=-t13;d[dp+12]=t13;d[dp+19]=-t24;d[dp+13]=t24;d[dp+18]=-t31;d[dp+14]=t31;d[dp+17]=-t02;d[dp+15]=t02;d[dp+16]=0};MP2.FRAME_SYNC=2047;MP2.VERSION={MPEG_2_5:0,MPEG_2:2,MPEG_1:3};MP2.LAYER={III:1,II:2,I:3};MP2.MODE={STEREO:0,JOINT_STEREO:1,DUAL_CHANNEL:2,MONO:3};MP2.SAMPLE_RATE=new Uint16Array([44100,48e3,32e3,0,22050,24e3,16e3,0]);MP2.BIT_RATE=new Uint16Array([32,48,56,64,80,96,112,128,160,192,224,256,320,384,8,16,24,32,40,48,56,64,80,96,112,128,144,160]);MP2.SCALEFACTOR_BASE=new Uint32Array([33554432,26632170,21137968]);MP2.SYNTHESIS_WINDOW=new Float32Array([0,-.5,-.5,-.5,-.5,-.5,-.5,-1,-1,-1,-1,-1.5,-1.5,-2,-2,-2.5,-2.5,-3,-3.5,-3.5,-4,-4.5,-5,-5.5,-6.5,-7,-8,-8.5,-9.5,-10.5,-12,-13,-14.5,-15.5,-17.5,-19,-20.5,-22.5,-24.5,-26.5,-29,-31.5,-34,-36.5,-39.5,-42.5,-45.5,-48.5,-52,-55.5,-58.5,-62.5,-66,-69.5,-73.5,-77,-80.5,-84.5,-88,-91.5,-95,-98,-101,-104,106.5,109,111,112.5,113.5,114,114,113.5,112,110.5,107.5,104,100,94.5,88.5,81.5,73,63.5,53,41.5,28.5,14.5,-1,-18,-36,-55.5,-76.5,-98.5,-122,-147,-173.5,-200.5,-229.5,-259.5,-290.5,-322.5,-355.5,-389.5,-424,-459.5,-495.5,-532,-568.5,-605,-641.5,-678,-714,-749,-783.5,-817,-849,-879.5,-908.5,-935,-959.5,-981,-1000.5,-1016,-1028.5,-1037.5,-1042.5,-1043.5,-1040,-1031.5,1018.5,1e3,976,946.5,911,869.5,822,767.5,707,640,565.5,485,397,302.5,201,92.5,-22.5,-144,-272.5,-407,-547.5,-694,-846,-1003,-1165,-1331.5,-1502,-1675.5,-1852.5,-2031.5,-2212.5,-2394,-2576.5,-2758.5,-2939.5,-3118.5,-3294.5,-3467.5,-3635.5,-3798.5,-3955,-4104.5,-4245.5,-4377.5,-4499,-4609.5,-4708,-4792.5,-4863.5,-4919,-4958,-4979.5,-4983,-4967.5,-4931.5,-4875,-4796,-4694.5,-4569.5,-4420,-4246,-4046,-3820,-3567,3287,2979.5,2644,2280.5,1888,1467.5,1018.5,541,35,-499,-1061,-1650,-2266.5,-2909,-3577,-4270,-4987.5,-5727.5,-6490,-7274,-8077.5,-8899.5,-9739,-10594.5,-11464.5,-12347,-13241,-14144.5,-15056,-15973.5,-16895.5,-17820,-18744.5,-19668,-20588,-21503,-22410.5,-23308.5,-24195,-25068.5,-25926.5,-26767,-27589,-28389,-29166.5,-29919,-30644.5,-31342,-32009.5,-32645,-33247,-33814.5,-34346,-34839.5,-35295,-35710,-36084.5,-36417.5,-36707.5,-36954,-37156.5,-37315,-37428,-37496,37519,37496,37428,37315,37156.5,36954,36707.5,36417.5,36084.5,35710,35295,34839.5,34346,33814.5,33247,32645,32009.5,31342,30644.5,29919,29166.5,28389,27589,26767,25926.5,25068.5,24195,23308.5,22410.5,21503,20588,19668,18744.5,17820,16895.5,15973.5,15056,14144.5,13241,12347,11464.5,10594.5,9739,8899.5,8077.5,7274,6490,5727.5,4987.5,4270,3577,2909,2266.5,1650,1061,499,-35,-541,-1018.5,-1467.5,-1888,-2280.5,-2644,-2979.5,3287,3567,3820,4046,4246,4420,4569.5,4694.5,4796,4875,4931.5,4967.5,4983,4979.5,4958,4919,4863.5,4792.5,4708,4609.5,4499,4377.5,4245.5,4104.5,3955,3798.5,3635.5,3467.5,3294.5,3118.5,2939.5,2758.5,2576.5,2394,2212.5,2031.5,1852.5,1675.5,1502,1331.5,1165,1003,846,694,547.5,407,272.5,144,22.5,-92.5,-201,-302.5,-397,-485,-565.5,-640,-707,-767.5,-822,-869.5,-911,-946.5,-976,-1e3,1018.5,1031.5,1040,1043.5,1042.5,1037.5,1028.5,1016,1000.5,981,959.5,935,908.5,879.5,849,817,783.5,749,714,678,641.5,605,568.5,532,495.5,459.5,424,389.5,355.5,322.5,290.5,259.5,229.5,200.5,173.5,147,122,98.5,76.5,55.5,36,18,1,-14.5,-28.5,-41.5,-53,-63.5,-73,-81.5,-88.5,-94.5,-100,-104,-107.5,-110.5,-112,-113.5,-114,-114,-113.5,-112.5,-111,-109,106.5,104,101,98,95,91.5,88,84.5,80.5,77,73.5,69.5,66,62.5,58.5,55.5,52,48.5,45.5,42.5,39.5,36.5,34,31.5,29,26.5,24.5,22.5,20.5,19,17.5,15.5,14.5,13,12,10.5,9.5,8.5,8,7,6.5,5.5,5,4.5,4,3.5,3.5,3,2.5,2.5,2,2,1.5,1.5,1,1,1,1,.5,.5,.5,.5,.5,.5]);MP2.QUANT_LUT_STEP_1=[[0,0,1,1,1,2,2,2,2,2,2,2,2,2],[0,0,0,0,0,0,1,1,1,2,2,2,2,2]];MP2.QUANT_TAB={A:27|64,B:30|64,C:8,D:12};MP2.QUANT_LUT_STEP_2=[[MP2.QUANT_TAB.C,MP2.QUANT_TAB.C,MP2.QUANT_TAB.D],[MP2.QUANT_TAB.A,MP2.QUANT_TAB.A,MP2.QUANT_TAB.A],[MP2.QUANT_TAB.B,MP2.QUANT_TAB.A,MP2.QUANT_TAB.B]];MP2.QUANT_LUT_STEP_3=[[68,68,52,52,52,52,52,52,52,52,52,52],[67,67,67,66,66,66,66,66,66,66,66,49,49,49,49,49,49,49,49,49,49,49,49,32,32,32,32,32,32,32],[69,69,69,69,52,52,52,52,52,52,52,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36]];MP2.QUANT_LUT_STEP4=[[0,1,2,17],[0,1,2,3,4,5,6,17],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,17],[0,1,3,5,6,7,8,9,10,11,12,13,14,15,16,17],[0,1,2,4,5,6,7,8,9,10,11,12,13,14,15,17],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]];MP2.QUANT_TAB=[{levels:3,group:1,bits:5},{levels:5,group:1,bits:7},{levels:7,group:0,bits:3},{levels:9,group:1,bits:10},{levels:15,group:0,bits:4},{levels:31,group:0,bits:5},{levels:63,group:0,bits:6},{levels:127,group:0,bits:7},{levels:255,group:0,bits:8},{levels:511,group:0,bits:9},{levels:1023,group:0,bits:10},{levels:2047,group:0,bits:11},{levels:4095,group:0,bits:12},{levels:8191,group:0,bits:13},{levels:16383,group:0,bits:14},{levels:32767,group:0,bits:15},{levels:65535,group:0,bits:16}];return MP2}();JSMpeg.Decoder.MP2AudioWASM=function(){"use strict";var MP2WASM=function(options){JSMpeg.Decoder.Base.call(this,options);this.onDecodeCallback=options.onAudioDecode;this.module=options.wasmModule;this.bufferSize=options.audioBufferSize||128*1024;this.bufferMode=options.streaming?JSMpeg.BitBuffer.MODE.EVICT:JSMpeg.BitBuffer.MODE.EXPAND;this.sampleRate=0};MP2WASM.prototype=Object.create(JSMpeg.Decoder.Base.prototype);MP2WASM.prototype.constructor=MP2WASM;MP2WASM.prototype.initializeWasmDecoder=function(){if(!this.module.instance){console.warn("JSMpeg: WASM module not compiled yet");return}this.instance=this.module.instance;this.functions=this.module.instance.exports;this.decoder=this.functions._mp2_decoder_create(this.bufferSize,this.bufferMode)};MP2WASM.prototype.destroy=function(){if(!this.decoder){return}this.functions._mp2_decoder_destroy(this.decoder)};MP2WASM.prototype.bufferGetIndex=function(){if(!this.decoder){return}return this.functions._mp2_decoder_get_index(this.decoder)};MP2WASM.prototype.bufferSetIndex=function(index){if(!this.decoder){return}this.functions._mp2_decoder_set_index(this.decoder,index)};MP2WASM.prototype.bufferWrite=function(buffers){if(!this.decoder){this.initializeWasmDecoder()}var totalLength=0;for(var i=0;i>4<<4;this.gl.viewport(0,0,codedWidth,this.height)};WebGLRenderer.prototype.createTexture=function(index,name){var gl=this.gl;var texture=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,texture);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);gl.uniform1i(gl.getUniformLocation(this.program,name),index);return texture};WebGLRenderer.prototype.createProgram=function(vsh,fsh){var gl=this.gl;var program=gl.createProgram();gl.attachShader(program,this.compileShader(gl.VERTEX_SHADER,vsh));gl.attachShader(program,this.compileShader(gl.FRAGMENT_SHADER,fsh));gl.linkProgram(program);gl.useProgram(program);return program};WebGLRenderer.prototype.compileShader=function(type,source){var gl=this.gl;var shader=gl.createShader(type);gl.shaderSource(shader,source);gl.compileShader(shader);if(!gl.getShaderParameter(shader,gl.COMPILE_STATUS)){throw new Error(gl.getShaderInfoLog(shader))}return shader};WebGLRenderer.prototype.allowsClampedTextureData=function(){var gl=this.gl;var texture=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,texture);gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,1,1,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,new Uint8ClampedArray([0]));return gl.getError()===0};WebGLRenderer.prototype.renderProgress=function(progress){var gl=this.gl;gl.useProgram(this.loadingProgram);var loc=gl.getUniformLocation(this.loadingProgram,"progress");gl.uniform1f(loc,progress);gl.drawArrays(gl.TRIANGLE_STRIP,0,4)};WebGLRenderer.prototype.render=function(y,cb,cr,isClampedArray){if(!this.enabled){return}var gl=this.gl;var w=this.width+15>>4<<4,h=this.height,w2=w>>1,h2=h>>1;if(isClampedArray&&this.shouldCreateUnclampedViews){y=new Uint8Array(y.buffer),cb=new Uint8Array(cb.buffer),cr=new Uint8Array(cr.buffer)}gl.useProgram(this.program);this.updateTexture(gl.TEXTURE0,this.textureY,w,h,y);this.updateTexture(gl.TEXTURE1,this.textureCb,w2,h2,cb);this.updateTexture(gl.TEXTURE2,this.textureCr,w2,h2,cr);gl.drawArrays(gl.TRIANGLE_STRIP,0,4)};WebGLRenderer.prototype.updateTexture=function(unit,texture,w,h,data){var gl=this.gl;gl.activeTexture(unit);gl.bindTexture(gl.TEXTURE_2D,texture);if(this.hasTextureData[unit]){gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data)}else{this.hasTextureData[unit]=true;gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,w,h,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data)}};WebGLRenderer.prototype.deleteTexture=function(unit,texture){var gl=this.gl;gl.activeTexture(unit);gl.bindTexture(gl.TEXTURE_2D,null);gl.deleteTexture(texture)};WebGLRenderer.IsSupported=function(){try{if(!window.WebGLRenderingContext){return false}var canvas=document.createElement("canvas");return!!(canvas.getContext("webgl")||canvas.getContext("experimental-webgl"))}catch(err){return false}};WebGLRenderer.SHADER={FRAGMENT_YCRCB_TO_RGBA:["precision mediump float;","uniform sampler2D textureY;","uniform sampler2D textureCb;","uniform sampler2D textureCr;","varying vec2 texCoord;","mat4 rec601 = mat4(","1.16438, 0.00000, 1.59603, -0.87079,","1.16438, -0.39176, -0.81297, 0.52959,","1.16438, 2.01723, 0.00000, -1.08139,","0, 0, 0, 1",");","void main() {","float y = texture2D(textureY, texCoord).r;","float cb = texture2D(textureCb, texCoord).r;","float cr = texture2D(textureCr, texCoord).r;","gl_FragColor = vec4(y, cr, cb, 1.0) * rec601;","}"].join("\n"),FRAGMENT_LOADING:["precision mediump float;","uniform float progress;","varying vec2 texCoord;","void main() {","float c = ceil(progress-(1.0-texCoord.y));","gl_FragColor = vec4(c,c,c,1);","}"].join("\n"),VERTEX_IDENTITY:["attribute vec2 vertex;","varying vec2 texCoord;","void main() {","texCoord = vertex;","gl_Position = vec4((vertex * 2.0 - 1.0) * vec2(1, -1), 0.0, 1.0);","}"].join("\n")};return WebGLRenderer}();JSMpeg.Renderer.Canvas2D=function(){"use strict";var CanvasRenderer=function(options){if(options.canvas){this.canvas=options.canvas;this.ownsCanvasElement=false}else{this.canvas=document.createElement("canvas");this.ownsCanvasElement=true}this.width=this.canvas.width;this.height=this.canvas.height;this.enabled=true;this.context=this.canvas.getContext("2d")};CanvasRenderer.prototype.destroy=function(){if(this.ownsCanvasElement){this.canvas.remove()}};CanvasRenderer.prototype.resize=function(width,height){this.width=width|0;this.height=height|0;this.canvas.width=this.width;this.canvas.height=this.height;this.imageData=this.context.getImageData(0,0,this.width,this.height);JSMpeg.Fill(this.imageData.data,255)};CanvasRenderer.prototype.renderProgress=function(progress){var w=this.canvas.width,h=this.canvas.height,ctx=this.context;ctx.fillStyle="#222";ctx.fillRect(0,0,w,h);ctx.fillStyle="#fff";ctx.fillRect(0,h-h*progress,w,h*progress)};CanvasRenderer.prototype.render=function(y,cb,cr){this.YCbCrToRGBA(y,cb,cr,this.imageData.data);this.context.putImageData(this.imageData,0,0)};CanvasRenderer.prototype.YCbCrToRGBA=function(y,cb,cr,rgba){if(!this.enabled){return}var w=this.width+15>>4<<4,w2=w>>1;var yIndex1=0,yIndex2=w,yNext2Lines=w+(w-this.width);var cIndex=0,cNextLine=w2-(this.width>>1);var rgbaIndex1=0,rgbaIndex2=this.width*4,rgbaNext2Lines=this.width*4;var cols=this.width>>1,rows=this.height>>1;var ccb,ccr,r,g,b;for(var row=0;row>8)-179;g=(ccr*88>>8)-44+(ccb*183>>8)-91;b=ccr+(ccr*198>>8)-227;var y1=y[yIndex1++];var y2=y[yIndex1++];rgba[rgbaIndex1]=y1+r;rgba[rgbaIndex1+1]=y1-g;rgba[rgbaIndex1+2]=y1+b;rgba[rgbaIndex1+4]=y2+r;rgba[rgbaIndex1+5]=y2-g;rgba[rgbaIndex1+6]=y2+b;rgbaIndex1+=8;var y3=y[yIndex2++];var y4=y[yIndex2++];rgba[rgbaIndex2]=y3+r;rgba[rgbaIndex2+1]=y3-g;rgba[rgbaIndex2+2]=y3+b;rgba[rgbaIndex2+4]=y4+r;rgba[rgbaIndex2+5]=y4-g;rgba[rgbaIndex2+6]=y4+b;rgbaIndex2+=8}yIndex1+=yNext2Lines;yIndex2+=yNext2Lines;rgbaIndex1+=rgbaNext2Lines;rgbaIndex2+=rgbaNext2Lines;cIndex+=cNextLine}};return CanvasRenderer}();JSMpeg.AudioOutput.WebAudio=function(){"use strict";var WebAudioOut=function(options){this.context=WebAudioOut.CachedContext=WebAudioOut.CachedContext||new(window.AudioContext||window.webkitAudioContext);this.gain=this.context.createGain();this.destination=this.gain;this.gain.connect(this.context.destination);this.context._connections=(this.context._connections||0)+1;this.startTime=0;this.buffer=null;this.wallclockStartTime=0;this.volume=1;this.enabled=true;this.unlocked=!WebAudioOut.NeedsUnlocking();Object.defineProperty(this,"enqueuedTime",{get:this.getEnqueuedTime})};WebAudioOut.prototype.destroy=function(){this.gain.disconnect();this.context._connections--;if(this.context._connections===0){this.context.close();WebAudioOut.CachedContext=null}};WebAudioOut.prototype.play=function(sampleRate,left,right){if(!this.enabled){return}if(!this.unlocked){var ts=JSMpeg.Now();if(this.wallclockStartTimethis.memory.buffer.byteLength){var bytesNeeded=this.brk-this.memory.buffer.byteLength;var pagesNeeded=Math.ceil(bytesNeeded/this.pageSize);this.memory.grow(pagesNeeded);this.createHeapViews()}return previousBrk};WASM.prototype.c_abort=function(size){console.warn("JSMPeg: WASM abort",arguments)};WASM.prototype.c_assertFail=function(size){console.warn("JSMPeg: WASM ___assert_fail",arguments)};WASM.prototype.readDylinkSection=function(buffer){var bytes=new Uint8Array(buffer);var next=0;var readVarUint=function(){var ret=0;var mul=1;while(1){var byte=bytes[next++];ret+=(byte&127)*mul;mul*=128;if(!(byte&128)){return ret}}};var matchNextBytes=function(expected){for(var i=0;i {}, + onClose = () => {}, + onMessage = () => {}, + onError = () => {}, + maxDelay = 30000 + } = options; + + let ws = null; + let reconnectDelay = 1000; + let reconnectTimer = null; + let isManualClose = false; + + function connect() { + ws = new WebSocket(url); + + ws.onopen = () => { + reconnectDelay = 1000; + onOpen(ws); + }; + + ws.onmessage = (e) => { + onMessage(e, ws); + }; + + ws.onclose = () => { + onClose(); + if (!isManualClose) { + reconnectTimer = setTimeout(() => { + reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); + connect(); + }, reconnectDelay); + } + }; + + ws.onerror = (err) => { + onError(err); + }; + } + + function close() { + isManualClose = true; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + if (ws) { + ws.close(); + } + } + + function getWebSocket() { + return ws; + } + + function isReady() { + return ws && ws.readyState === WebSocket.OPEN; + } + + connect(); + + return { + getWebSocket, + isReady, + close + }; +} diff --git a/remote/scripts/migrate-password.js b/remote/scripts/migrate-password.js new file mode 100644 index 0000000..770011c --- /dev/null +++ b/remote/scripts/migrate-password.js @@ -0,0 +1,55 @@ +const bcrypt = require('bcryptjs'); +const readline = require('readline'); + +const SALT_ROUNDS = 10; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +function question(prompt) { + return new Promise((resolve) => { + rl.question(prompt, resolve); + }); +} + +async function hashPassword(password) { + return bcrypt.hash(password, SALT_ROUNDS); +} + +async function main() { + console.log('=== 密码迁移脚本 ==='); + console.log('此脚本将明文密码转换为 bcrypt 哈希值\n'); + + let continueInput = true; + + while (continueInput) { + const password = await question('请输入要哈希的密码 (或输入 q 退出): '); + + if (password.toLowerCase() === 'q') { + continueInput = false; + continue; + } + + if (!password.trim()) { + console.log('密码不能为空,请重新输入\n'); + continue; + } + + try { + const hashedPassword = await hashPassword(password); + console.log('\n--- 结果 ---'); + console.log(`明文密码: ${password}`); + console.log(`bcrypt 哈希: ${hashedPassword}`); + console.log('\n请将哈希值更新到环境变量或配置文件中\n'); + } catch (error) { + console.error('哈希失败:', error.message); + } + } + + console.log('\n迁移完成!'); + rl.close(); +} + +main().catch(console.error); diff --git a/remote/src/config/index.js b/remote/src/config/index.js new file mode 100644 index 0000000..663122e --- /dev/null +++ b/remote/src/config/index.js @@ -0,0 +1,153 @@ +const path = require('path'); +const fs = require('fs'); +const { validate, getDefaults, mergeWithDefaults } = require('./schema'); +const paths = require('../utils/paths'); + +let cachedConfig = null; + +function getConfigPath() { + const configDir = paths.getConfigPath(); + return path.join(configDir, 'default.json'); +} + +function loadConfigFile() { + const configPath = getConfigPath(); + + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, 'utf8'); + const parsed = JSON.parse(content); + return parsed; + } + } catch (error) { + console.error(`Failed to load config file: ${error.message}`); + } + + return {}; +} + +function getEnvOverrides() { + const overrides = {}; + const envPrefix = 'REMOTE_'; + + for (const [envKey, envValue] of Object.entries(process.env)) { + if (!envKey.startsWith(envPrefix)) continue; + + const parts = envKey.slice(envPrefix.length).split('_'); + if (parts.length < 2) continue; + + const section = parts[0].toLowerCase(); + const configKey = parts.slice(1).join('_').toLowerCase(); + + if (!overrides[section]) { + overrides[section] = {}; + } + + let parsedValue = envValue; + + if (envValue === 'true') { + parsedValue = true; + } else if (envValue === 'false') { + parsedValue = false; + } else if (!isNaN(envValue) && envValue !== '') { + parsedValue = parseFloat(envValue); + if (Number.isInteger(parsedValue)) { + parsedValue = parseInt(envValue, 10); + } + } + + overrides[section][configKey] = parsedValue; + } + + return overrides; +} + +function applyEnvOverrides(config, envOverrides) { + const result = { ...config }; + + for (const [section, values] of Object.entries(envOverrides)) { + if (!result[section]) { + result[section] = {}; + } + + for (const [key, value] of Object.entries(values)) { + result[section][key] = value; + } + } + + return result; +} + +function loadConfig() { + if (cachedConfig) { + return cachedConfig; + } + + const defaults = getDefaults(); + const fileConfig = loadConfigFile(); + const envOverrides = getEnvOverrides(); + + let merged = mergeWithDefaults(fileConfig); + merged = applyEnvOverrides(merged, envOverrides); + + const validation = validate(merged); + if (!validation.valid) { + console.warn('Config validation warnings:', validation.errors); + } + + cachedConfig = merged; + return cachedConfig; +} + +function get(key, defaultValue = undefined) { + const config = loadConfig(); + + if (!key) { + return config; + } + + const keys = key.split('.'); + let result = config; + + for (const k of keys) { + if (result && typeof result === 'object' && k in result) { + result = result[k]; + } else { + return defaultValue; + } + } + + return result; +} + +function getSection(section) { + const config = loadConfig(); + + if (!section) { + return config; + } + + return config[section] || null; +} + +function getAll() { + return loadConfig(); +} + +function reload() { + cachedConfig = null; + return loadConfig(); +} + +function clearCache() { + cachedConfig = null; +} + +module.exports = { + get, + getSection, + getAll, + reload, + clearCache, + validate +}; diff --git a/remote/src/config/schema.js b/remote/src/config/schema.js new file mode 100644 index 0000000..726a4cf --- /dev/null +++ b/remote/src/config/schema.js @@ -0,0 +1,201 @@ +const defaultConfig = { + server: { + port: 3000, + host: '0.0.0.0' + }, + stream: { + fps: 30, + bitrate: '4000k', + gop: 10, + preset: 'ultrafast', + resolution: { + width: 1920, + height: 1080 + } + }, + input: { + mouseEnabled: true, + keyboardEnabled: true, + sensitivity: 1.0 + }, + security: { + password: '', + tokenExpiry: 3600 + }, + frp: { + enabled: true + }, + gitea: { + enabled: true + } +}; + +const schema = { + server: { + type: 'object', + properties: { + port: { type: 'number', required: true, min: 1, max: 65535 }, + host: { type: 'string', required: true } + } + }, + stream: { + type: 'object', + properties: { + fps: { type: 'number', required: true, min: 1, max: 120 }, + bitrate: { type: 'string', required: true }, + gop: { type: 'number', required: true, min: 1 }, + preset: { type: 'string', required: true }, + resolution: { + type: 'object', + properties: { + width: { type: 'number', required: true, min: 1 }, + height: { type: 'number', required: true, min: 1 } + } + } + } + }, + input: { + type: 'object', + properties: { + mouseEnabled: { type: 'boolean', required: true }, + keyboardEnabled: { type: 'boolean', required: true }, + sensitivity: { type: 'number', required: true, min: 0.1, max: 10 } + } + }, + security: { + type: 'object', + properties: { + password: { type: 'string', required: false }, + tokenExpiry: { type: 'number', required: true, min: 60 } + } + }, + frp: { + type: 'object', + properties: { + enabled: { type: 'boolean', required: true } + } + }, + gitea: { + type: 'object', + properties: { + enabled: { type: 'boolean', required: true } + } + } +}; + +function validateType(value, expectedType) { + if (expectedType === 'number') { + return typeof value === 'number' && !isNaN(value); + } + if (expectedType === 'string') { + return typeof value === 'string'; + } + if (expectedType === 'boolean') { + return typeof value === 'boolean'; + } + if (expectedType === 'object') { + return value !== null && typeof value === 'object' && !Array.isArray(value); + } + return false; +} + +function validateField(value, fieldSchema, fieldPath) { + const errors = []; + + if (!validateType(value, fieldSchema.type)) { + errors.push(`${fieldPath}: expected type ${fieldSchema.type}, got ${typeof value}`); + return errors; + } + + if (fieldSchema.type === 'number') { + if (fieldSchema.min !== undefined && value < fieldSchema.min) { + errors.push(`${fieldPath}: value ${value} is less than minimum ${fieldSchema.min}`); + } + if (fieldSchema.max !== undefined && value > fieldSchema.max) { + errors.push(`${fieldPath}: value ${value} is greater than maximum ${fieldSchema.max}`); + } + } + + if (fieldSchema.type === 'object' && fieldSchema.properties) { + const nestedErrors = validateObject(value, fieldSchema, fieldPath); + errors.push(...nestedErrors); + } + + return errors; +} + +function validateObject(obj, objectSchema, basePath) { + const errors = []; + + if (!objectSchema.properties) return errors; + + for (const [key, fieldSchema] of Object.entries(objectSchema.properties)) { + const fieldPath = basePath ? `${basePath}.${key}` : key; + + if (!(key in obj)) { + if (fieldSchema.required) { + errors.push(`${fieldPath}: required field is missing`); + } + continue; + } + + const fieldErrors = validateField(obj[key], fieldSchema, fieldPath); + errors.push(...fieldErrors); + } + + return errors; +} + +function validate(config) { + const errors = []; + + if (!config || typeof config !== 'object') { + return { valid: false, errors: ['Config must be a non-null object'] }; + } + + for (const [sectionKey, sectionSchema] of Object.entries(schema)) { + if (!(sectionKey in config)) { + errors.push(`${sectionKey}: required section is missing`); + continue; + } + + const sectionErrors = validateObject(config[sectionKey], sectionSchema, sectionKey); + errors.push(...sectionErrors); + } + + return { + valid: errors.length === 0, + errors + }; +} + +function getDefaults() { + return JSON.parse(JSON.stringify(defaultConfig)); +} + +function mergeWithDefaults(config) { + const defaults = getDefaults(); + return deepMerge(defaults, config); +} + +function deepMerge(target, source) { + const result = { ...target }; + + for (const key of Object.keys(source)) { + if (source[key] instanceof Object && key in target && target[key] instanceof Object) { + result[key] = deepMerge(target[key], source[key]); + } else { + result[key] = source[key]; + } + } + + return result; +} + +module.exports = { + schema, + defaultConfig, + validate, + getDefaults, + mergeWithDefaults +}; diff --git a/remote/src/controllers/AuthController.js b/remote/src/controllers/AuthController.js new file mode 100644 index 0000000..5343957 --- /dev/null +++ b/remote/src/controllers/AuthController.js @@ -0,0 +1,97 @@ +const AuthService = require('../services/auth/AuthService'); +const TokenManager = require('../services/auth/TokenManager'); +const logger = require('../utils/logger'); + +class AuthController { + constructor() { + this.authService = AuthService.getInstance(); + this.tokenManager = TokenManager.getInstance(); + } + + async login(req, res) { + const { password } = req.body; + + if (!this.authService.hasPassword()) { + const token = this.tokenManager.generateToken({ userId: 'default-user' }); + logger.info('Login successful: no password configured'); + return res.json({ + success: true, + token, + message: 'Authentication disabled' + }); + } + + if (!password) { + logger.warn('Login failed: no password provided'); + return res.status(400).json({ + success: false, + error: 'Password is required' + }); + } + + try { + const isValid = await this.authService.authenticate(password); + + if (isValid) { + const token = this.tokenManager.generateToken({ userId: 'default-user' }); + logger.info('Login successful'); + return res.json({ + success: true, + token + }); + } else { + logger.warn('Login failed: invalid password'); + return res.status(401).json({ + success: false, + error: 'Invalid password' + }); + } + } catch (error) { + logger.error('Login error', { error: error.message }); + return res.status(500).json({ + success: false, + error: 'Authentication failed' + }); + } + } + + async verify(req, res) { + const authHeader = req.headers.authorization; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ + success: false, + valid: false, + error: 'No token provided' + }); + } + + try { + const decoded = this.tokenManager.verifyToken(token); + + if (decoded) { + return res.json({ + success: true, + valid: true, + userId: decoded.userId + }); + } else { + return res.status(401).json({ + success: false, + valid: false, + error: 'Invalid or expired token' + }); + } + } catch (error) { + logger.error('Token verification error', { error: error.message }); + return res.status(500).json({ + success: false, + valid: false, + error: 'Token verification failed' + }); + } + } +} + +module.exports = AuthController; diff --git a/remote/src/controllers/InputController.js b/remote/src/controllers/InputController.js new file mode 100644 index 0000000..2cdd4a4 --- /dev/null +++ b/remote/src/controllers/InputController.js @@ -0,0 +1,86 @@ +const { PowerShellInput } = require('../services/input'); +const logger = require('../utils/logger'); + +class InputController { + constructor() { + this.inputService = new PowerShellInput(); + } + + async mouseMove(x, y) { + try { + await this.inputService.mouseMove(x, y); + } catch (error) { + logger.error('Mouse move failed', { error: error.message, x, y }); + } + } + + async mouseDown(button = 'left') { + try { + await this.inputService.mouseDown(button); + } catch (error) { + logger.error('Mouse down failed', { error: error.message, button }); + } + } + + async mouseUp(button = 'left') { + try { + await this.inputService.mouseUp(button); + } catch (error) { + logger.error('Mouse up failed', { error: error.message, button }); + } + } + + async mouseClick(button = 'left') { + try { + await this.inputService.mouseClick(button); + } catch (error) { + logger.error('Mouse click failed', { error: error.message, button }); + } + } + + async mouseWheel(delta) { + try { + await this.inputService.mouseWheel(delta); + } catch (error) { + logger.error('Mouse wheel failed', { error: error.message, delta }); + } + } + + async keyDown(key) { + try { + await this.inputService.keyDown(key); + } catch (error) { + logger.error('Key down failed', { error: error.message, key }); + } + } + + async keyUp(key) { + try { + await this.inputService.keyUp(key); + } catch (error) { + logger.error('Key up failed', { error: error.message, key }); + } + } + + async keyPress(key) { + try { + await this.inputService.keyPress(key); + } catch (error) { + logger.error('Key press failed', { error: error.message, key }); + } + } + + async keyType(text) { + try { + await this.inputService.keyType(text); + } catch (error) { + logger.error('Key type failed', { error: error.message, text }); + } + } + + stop() { + this.inputService.stop(); + } +} + +module.exports = InputController; diff --git a/remote/src/controllers/StreamController.js b/remote/src/controllers/StreamController.js new file mode 100644 index 0000000..5275bfb --- /dev/null +++ b/remote/src/controllers/StreamController.js @@ -0,0 +1,102 @@ +const FFmpegEncoder = require('../services/stream/FFmpegEncoder'); +const logger = require('../utils/logger'); + +class StreamController { + constructor() { + this.encoder = null; + } + + _getEncoder() { + if (!this.encoder) { + this.encoder = new FFmpegEncoder(); + } + return this.encoder; + } + + getInfo(req, res) { + try { + const encoder = this._getEncoder(); + const resolution = encoder.getScreenResolution(); + const isRunning = encoder.isRunning(); + + const info = { + success: true, + stream: { + status: isRunning ? 'running' : 'stopped', + resolution: { + width: resolution.width, + height: resolution.height + }, + fps: encoder.fps, + bitrate: encoder.bitrate, + gop: encoder.gop, + encoder: encoder.getEncoder() + } + }; + + return res.json(info); + } catch (error) { + logger.error('Failed to get stream info', { error: error.message }); + return res.status(500).json({ + success: false, + error: 'Failed to get stream info' + }); + } + } + + start(req, res) { + try { + const encoder = this._getEncoder(); + + if (encoder.isRunning()) { + return res.json({ + success: true, + message: 'Stream is already running' + }); + } + + encoder.start(); + logger.info('Stream started'); + + return res.json({ + success: true, + message: 'Stream started' + }); + } catch (error) { + logger.error('Failed to start stream', { error: error.message }); + return res.status(500).json({ + success: false, + error: 'Failed to start stream' + }); + } + } + + stop(req, res) { + try { + const encoder = this._getEncoder(); + + if (!encoder.isRunning()) { + return res.json({ + success: true, + message: 'Stream is not running' + }); + } + + encoder.stop(); + logger.info('Stream stopped'); + + return res.json({ + success: true, + message: 'Stream stopped' + }); + } catch (error) { + logger.error('Failed to stop stream', { error: error.message }); + return res.status(500).json({ + success: false, + error: 'Failed to stop stream' + }); + } + } +} + +module.exports = StreamController; diff --git a/remote/src/core/App.js b/remote/src/core/App.js new file mode 100644 index 0000000..a2e51f5 --- /dev/null +++ b/remote/src/core/App.js @@ -0,0 +1,454 @@ +const Container = require('./Container'); +const EventBus = require('./EventBus'); +const EventTypes = require('./events'); +const { ErrorHandler } = require('./ErrorHandler'); +const logger = require('../utils/logger'); +const MessageTypes = require('../server/messageTypes'); + +const SHUTDOWN_TIMEOUT = 30000; + +class App { + constructor() { + this.container = null; + this.eventBus = null; + this.started = false; + this.shuttingDown = false; + this.lastClipboardText = ''; + } + + async bootstrap() { + this.container = new Container(); + this.eventBus = new EventBus(); + + this.container.register('eventBus', () => this.eventBus); + + this._registerConfig(); + this._registerLogger(); + this._registerErrorHandler(); + this._registerAuthServices(); + this._registerStreamServices(); + this._registerInputServices(); + this._registerClipboardServices(); + this._registerFileServices(); + this._registerNetworkServices(); + this._registerServer(); + + logger.info('Application bootstrap completed'); + + return this; + } + + _registerConfig() { + this.container.register('config', (c) => { + return require('../config'); + }); + } + + _registerLogger() { + this.container.register('logger', (c) => { + return require('../utils/logger'); + }); + } + + _registerEventBus() { + this.container.register('eventBus', (c) => { + return this.eventBus; + }); + } + + _registerErrorHandler() { + this.container.register('errorHandler', (c) => { + return new ErrorHandler(); + }); + } + + _registerAuthServices() { + this.container.register('authService', (c) => { + const AuthService = require('../services/auth/AuthService'); + return new AuthService(); + }); + + this.container.register('tokenManager', (c) => { + const TokenManager = require('../services/auth/TokenManager'); + return new TokenManager(); + }); + } + + _registerStreamServices() { + this.container.register('ffmpegEncoder', (c) => { + const FFmpegEncoder = require('../services/stream/FFmpegEncoder'); + return new FFmpegEncoder(); + }); + } + + _registerInputServices() { + this.container.register('inputService', (c) => { + const PowerShellInput = require('../services/input/PowerShellInput'); + return new PowerShellInput(); + }); + + this.container.register('inputHandler', (c) => { + const InputHandler = require('../server/InputHandler'); + const inputService = c.resolve('inputService'); + return new InputHandler(inputService); + }); + } + + _registerClipboardServices() { + this.container.register('clipboardService', (c) => { + const { clipboardService } = require('../services/clipboard'); + return clipboardService; + }); + } + + _registerFileServices() { + this.container.register('fileService', (c) => { + const { fileService } = require('../services/file'); + return fileService; + }); + } + + _registerNetworkServices() { + this.container.register('frpService', (c) => { + const FRPService = require('../services/network/FRPService'); + const config = c.resolve('config'); + const frpConfig = config.getSection('frp') || {}; + return new FRPService({ + enabled: frpConfig.enabled !== false + }); + }); + + this.container.register('giteaService', (c) => { + const GiteaService = require('../services/network/GiteaService'); + const config = c.resolve('config'); + const giteaConfig = config.getSection('gitea') || {}; + return new GiteaService({ + enabled: giteaConfig.enabled !== false + }); + }); + } + + _registerServer() { + this.container.register('httpServer', (c) => { + const Server = require('../server/Server'); + const config = c.resolve('config'); + const serverConfig = config.getSection('server') || {}; + return new Server({ + port: serverConfig.port || 3000, + host: serverConfig.host || '0.0.0.0' + }); + }); + + this.container.register('wsServer', (c) => { + const WebSocketServer = require('../server/WebSocketServer'); + return new WebSocketServer(); + }); + + this.container.register('streamBroadcaster', (c) => { + const StreamBroadcaster = require('../server/StreamBroadcaster'); + const wsServer = c.resolve('wsServer'); + return new StreamBroadcaster(wsServer); + }); + } + + async start() { + if (this.started) { + logger.warn('Application already started'); + return; + } + + const errorHandler = this.container.resolve('errorHandler'); + errorHandler.initialize(); + + this._setupRoutes(); + + const httpServer = this.container.resolve('httpServer'); + const address = await httpServer.start(); + logger.info('HTTP server started', { address }); + + const wsServer = this.container.resolve('wsServer'); + wsServer.start(httpServer.getHTTPServer()); + logger.info('WebSocket server started'); + + this._setupWebSocketHandlers(); + + const ffmpegEncoder = this.container.resolve('ffmpegEncoder'); + ffmpegEncoder.start(); + logger.info('FFmpeg encoder started'); + + const screenRes = ffmpegEncoder.getScreenResolution(); + wsServer.setScreenResolution(screenRes.width, screenRes.height); + + const streamBroadcaster = this.container.resolve('streamBroadcaster'); + streamBroadcaster.setEncoder(ffmpegEncoder); + logger.info('Stream broadcaster attached to encoder'); + + const inputService = this.container.resolve('inputService'); + await inputService.start(); + logger.info('Input service started'); + + const frpService = this.container.resolve('frpService'); + frpService.start(); + logger.info('FRP service started'); + + const giteaService = this.container.resolve('giteaService'); + giteaService.start(); + logger.info('Gitea service started'); + + // 启动剪贴板监控,主动同步到主控 + this._startClipboardWatcher(); + + this.started = true; + + await this.eventBus.emit(EventTypes.APP_START, { + timestamp: new Date().toISOString(), + address + }); + + logger.info('Application started successfully'); + + this._setupGracefulShutdown(); + } + + _setupRoutes() { + const httpServer = this.container.resolve('httpServer'); + const config = this.container.resolve('config'); + const authService = this.container.resolve('authService'); + const tokenManager = this.container.resolve('tokenManager'); + const inputHandler = this.container.resolve('inputHandler'); + const paths = require('../utils/paths'); + + const express = require('express'); + const cookieParser = require('cookie-parser'); + const authMiddleware = require('../middlewares/auth'); + const routes = require('../routes'); + + httpServer.use(cookieParser()); + httpServer.use(express.json()); + httpServer.use(express.urlencoded({ extended: true })); + + httpServer.app.post('/login', async (req, res) => { + const { password } = req.body; + const isValid = await authService.authenticate(password); + + if (isValid) { + const token = tokenManager.generateToken({ userId: 'default-user' }); + res.cookie('auth', token, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }); + return res.redirect('/'); + } + + httpServer.renderLoginPage(res, '密码错误'); + }); + + httpServer.use((req, res, next) => { + if (!authService.hasPassword()) { + res.locals.authenticated = true; + return next(); + } + + const token = req.cookies && req.cookies.auth; + if (token) { + const decoded = tokenManager.verifyToken(token); + if (decoded) { + res.locals.authenticated = true; + return next(); + } + } + + if (req.path === '/' || req.path === '/index.html') { + return httpServer.renderLoginPage(res); + } + + res.status(401).json({ error: 'Authentication required' }); + }); + + httpServer.static(paths.getPublicPath()); + + httpServer.use('/api', authMiddleware); + httpServer.use('/api', routes); + + httpServer.app.get('/api/config', (req, res) => { + try { + res.json(config.getAll()); + } catch (error) { + logger.error('Error getting config', { error: error.message }); + res.status(500).json({ error: 'Failed to get config' }); + } + }); + + const wsServer = this.container.resolve('wsServer'); + const originalSetup = wsServer.setupConnectionHandler.bind(wsServer); + wsServer.setupConnectionHandler = function() { + originalSetup(); + const originalHandlers = this.wss.listeners('connection'); + this.wss.removeAllListeners('connection'); + + const securityConfig = require('../utils/config').getSecurityConfig(); + const password = securityConfig.password; + + // 未认证的连接也允许,用于剪贴板同步 + this.wss.on('connection', (ws, req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const isAuthenticated = url.searchParams.get('password') === password; + + // 保存认证状态 + ws.isAuthenticated = isAuthenticated; + + // 处理输入消息(不检查认证) + ws.on('message', (data) => { + try { + const message = JSON.parse(data); + inputHandler.handleMessage(message, ws); + } catch (error) { + logger.debug('Failed to parse WebSocket message', { error: error.message }); + } + }); + + // 调用原始 handlers(用于已认证的连接) + originalHandlers.forEach(handler => { + handler(ws, req); + }); + }); + }; + } + + _setupWebSocketHandlers() { + // WebSocket handlers set up in _ aresetupRoutes() + // to avoid duplicate message handling + } + + _startClipboardWatcher() { + const clipboardService = this.container.resolve('clipboardService'); + const wsServer = this.container.resolve('wsServer'); + + clipboardService.read().then(content => { + if (content.type === 'text') { + this.lastClipboardText = content.data || ''; + } + }).catch(() => {}); + + setInterval(async () => { + try { + const content = await clipboardService.read(); + if (content.type === 'text' && content.data !== this.lastClipboardText) { + this.lastClipboardText = content.data || ''; + + const message = JSON.stringify({ + type: MessageTypes.CLIPBOARD_DATA, + contentType: content.type, + data: content.data, + size: content.size + }); + + wsServer.clients.forEach((client) => { + if (client.readyState === 1) { // WebSocket.OPEN + client.send(message); + } + }); + logger.info('Clipboard changed, synced to client'); + } + } catch (error) { + // ignore + } + }, 1000); + } + + _setupGracefulShutdown() { + const shutdown = async (signal) => { + if (this.shuttingDown) { + return; + } + this.shuttingDown = true; + + logger.info(`Received ${signal}, starting graceful shutdown`); + await this.stop(); + process.exit(0); + }; + + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); + + process.on('exit', () => { + logger.info('Process exiting'); + }); + } + + async stop() { + if (!this.started) { + return; + } + + logger.info('Stopping application'); + + const shutdownPromise = async () => { + const frpService = this.container.resolve('frpService'); + frpService.stop(); + logger.info('FRP service stopped'); + + const giteaService = this.container.resolve('giteaService'); + giteaService.stop(); + logger.info('Gitea service stopped'); + + const streamBroadcaster = this.container.resolve('streamBroadcaster'); + streamBroadcaster.stop(); + logger.info('Stream broadcaster stopped'); + + const ffmpegEncoder = this.container.resolve('ffmpegEncoder'); + ffmpegEncoder.stop(); + logger.info('FFmpeg encoder stopped'); + + const inputHandler = this.container.resolve('inputHandler'); + inputHandler.stop(); + logger.info('Input handler stopped'); + + const inputService = this.container.resolve('inputService'); + await inputService.stop(); + logger.info('Input service stopped'); + + const wsServer = this.container.resolve('wsServer'); + wsServer.stop(); + logger.info('WebSocket server stopped'); + + const httpServer = this.container.resolve('httpServer'); + await httpServer.stop(); + logger.info('HTTP server stopped'); + + await this.eventBus.emit(EventTypes.APP_STOP, { + timestamp: new Date().toISOString() + }); + + this.started = false; + logger.info('Application stopped successfully'); + }; + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Shutdown timeout exceeded')); + }, SHUTDOWN_TIMEOUT); + }); + + try { + await Promise.race([shutdownPromise(), timeoutPromise]); + } catch (error) { + logger.error('Error during shutdown', { error: error.message }); + this.started = false; + } + } + + getService(name) { + if (!this.container) { + throw new Error('Application not bootstrapped'); + } + return this.container.resolve(name); + } + + getEventBus() { + return this.eventBus; + } + + isStarted() { + return this.started; + } +} + +module.exports = App; diff --git a/remote/src/core/Container.js b/remote/src/core/Container.js new file mode 100644 index 0000000..c49a5eb --- /dev/null +++ b/remote/src/core/Container.js @@ -0,0 +1,77 @@ +class Container { + constructor() { + this._services = new Map(); + this._singletons = new Map(); + this._resolving = new Set(); + } + + register(name, factory, isSingleton = true) { + if (typeof name !== 'string' || name.trim() === '') { + throw new Error('Service name must be a non-empty string'); + } + if (typeof factory !== 'function') { + throw new Error('Factory must be a function'); + } + if (this._services.has(name)) { + throw new Error(`Service "${name}" is already registered`); + } + + this._services.set(name, { factory, isSingleton }); + return this; + } + + resolve(name) { + if (!this._services.has(name)) { + throw new Error(`Service "${name}" is not registered`); + } + + if (this._resolving.has(name)) { + const chain = Array.from(this._resolving).join(' -> '); + throw new Error( + `Circular dependency detected: ${chain} -> ${name}. ` + + `This creates a circular reference that cannot be resolved.` + ); + } + + const service = this._services.get(name); + + if (service.isSingleton && this._singletons.has(name)) { + return this._singletons.get(name); + } + + this._resolving.add(name); + + try { + const instance = service.factory(this); + + if (service.isSingleton) { + this._singletons.set(name, instance); + } + + return instance; + } finally { + this._resolving.delete(name); + } + } + + has(name) { + return this._services.has(name); + } + + unregister(name) { + if (!this._services.has(name)) { + return false; + } + this._services.delete(name); + this._singletons.delete(name); + return true; + } + + clear() { + this._services.clear(); + this._singletons.clear(); + this._resolving.clear(); + } +} + +module.exports = Container; diff --git a/remote/src/core/ErrorHandler.js b/remote/src/core/ErrorHandler.js new file mode 100644 index 0000000..38f0f8e --- /dev/null +++ b/remote/src/core/ErrorHandler.js @@ -0,0 +1,81 @@ +const logger = require('../utils/logger'); + +function createErrorResponse(error, code, details) { + const response = { + error: error instanceof Error ? error.message : String(error) + }; + + if (code) { + response.code = code; + } + + if (details !== undefined) { + response.details = details; + } + + return response; +} + +class ErrorHandler { + constructor() { + this.initialized = false; + } + + initialize() { + if (this.initialized) { + return; + } + + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', { + message: error.message, + stack: error.stack, + name: error.name + }); + + setTimeout(() => { + process.exit(1); + }, 1000); + }); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection:', { + reason: reason instanceof Error + ? { message: reason.message, stack: reason.stack, name: reason.name } + : reason, + promise: String(promise) + }); + }); + + this.initialized = true; + logger.info('Global error handlers initialized'); + } + + handleError(error, context = {}) { + const errorInfo = { + message: error.message, + stack: error.stack, + name: error.name, + context + }; + + logger.error('Error occurred:', errorInfo); + + return createErrorResponse(error); + } + + createError(message, code, details) { + const error = new Error(message); + error.code = code; + error.details = details; + return error; + } +} + +const errorHandler = new ErrorHandler(); + +module.exports = { + ErrorHandler, + errorHandler, + createErrorResponse +}; diff --git a/remote/src/core/EventBus.js b/remote/src/core/EventBus.js new file mode 100644 index 0000000..544bd01 --- /dev/null +++ b/remote/src/core/EventBus.js @@ -0,0 +1,100 @@ +class EventBus { + constructor() { + this._listeners = new Map(); + } + + on(event, handler) { + if (typeof event !== 'string' || event.trim() === '') { + throw new Error('Event name must be a non-empty string'); + } + if (typeof handler !== 'function') { + throw new Error('Handler must be a function'); + } + + if (!this._listeners.has(event)) { + this._listeners.set(event, []); + } + + this._listeners.get(event).push(handler); + return this; + } + + off(event, handler) { + if (!this._listeners.has(event)) { + return false; + } + + const handlers = this._listeners.get(event); + const index = handlers.indexOf(handler); + + if (index === -1) { + return false; + } + + handlers.splice(index, 1); + + if (handlers.length === 0) { + this._listeners.delete(event); + } + + return true; + } + + async emit(event, data) { + if (!this._listeners.has(event)) { + return []; + } + + const handlers = this._listeners.get(event); + const results = []; + + for (const handler of handlers) { + try { + const result = await handler(data); + results.push(result); + } catch (error) { + results.push({ error }); + } + } + + return results; + } + + once(event, handler) { + if (typeof event !== 'string' || event.trim() === '') { + throw new Error('Event name must be a non-empty string'); + } + if (typeof handler !== 'function') { + throw new Error('Handler must be a function'); + } + + const onceHandler = async (data) => { + this.off(event, onceHandler); + return await handler(data); + }; + + return this.on(event, onceHandler); + } + + removeAllListeners(event) { + if (event === undefined) { + this._listeners.clear(); + return true; + } + + if (typeof event !== 'string') { + throw new Error('Event name must be a string'); + } + + return this._listeners.delete(event); + } + + listenerCount(event) { + if (!this._listeners.has(event)) { + return 0; + } + return this._listeners.get(event).length; + } +} + +module.exports = EventBus; diff --git a/remote/src/core/events.js b/remote/src/core/events.js new file mode 100644 index 0000000..b9a4c5b --- /dev/null +++ b/remote/src/core/events.js @@ -0,0 +1,27 @@ +const EventTypes = { + STREAM_START: 'stream:start', + STREAM_STOP: 'stream:stop', + STREAM_DATA: 'stream:data', + STREAM_ERROR: 'stream:error', + CLIENT_CONNECTED: 'client:connected', + CLIENT_DISCONNECTED: 'client:disconnected', + INPUT_EVENT: 'input:event', + APP_START: 'app:start', + APP_STOP: 'app:stop', + ERROR: 'error', + + // Agent events + AGENT_CONNECTED: 'agent:connected', + AGENT_DISCONNECTED: 'agent:disconnected', + AGENT_REGISTERED: 'agent:registered', + AGENT_HEARTBEAT: 'agent:heartbeat', + AGENT_STREAM_START: 'agent:stream:start', + AGENT_STREAM_STOP: 'agent:stream:stop', + + // Controller events + CONTROLLER_CONNECTED: 'controller:connected', + CONTROLLER_DISCONNECTED: 'controller:disconnected', + CONTROLLER_STREAM_SWITCH: 'controller:stream:switch' +}; + +module.exports = EventTypes; diff --git a/remote/src/index.js b/remote/src/index.js new file mode 100644 index 0000000..970107f --- /dev/null +++ b/remote/src/index.js @@ -0,0 +1,37 @@ +const App = require('./core/App'); +const { ErrorHandler } = require('./core/ErrorHandler'); +const logger = require('./utils/logger'); + +const errorHandler = new ErrorHandler(); +errorHandler.initialize(); + +const app = new App(); + +async function main() { + try { + await app.bootstrap(); + await app.start(); + } catch (error) { + logger.error('Failed to start application', { error: error.message, stack: error.stack }); + process.exit(1); + } +} + +async function gracefulShutdown(signal) { + if (app.isStarted()) { + logger.info(`Received ${signal}, shutting down gracefully`); + await app.stop(); + } + process.exit(0); +} + +process.on('SIGINT', () => gracefulShutdown('SIGINT')); +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + +process.on('exit', () => { + logger.info('Process exiting'); +}); + +main(); + +module.exports = app; diff --git a/remote/src/middlewares/auth.js b/remote/src/middlewares/auth.js new file mode 100644 index 0000000..fe66280 --- /dev/null +++ b/remote/src/middlewares/auth.js @@ -0,0 +1,80 @@ +const AuthService = require('../services/auth/AuthService'); +const TokenManager = require('../services/auth/TokenManager'); +const logger = require('../utils/logger'); + +function extractToken(req) { + if (req.headers.authorization) { + const parts = req.headers.authorization.split(' '); + if (parts.length === 2 && parts[0] === 'Bearer') { + return parts[1]; + } + } + + if (req.cookies) { + if (req.cookies.token) { + return req.cookies.token; + } + if (req.cookies.auth) { + return req.cookies.auth; + } + } + + if (req.query && req.query.token) { + return req.query.token; + } + + return null; +} + +async function authMiddleware(req, res, next) { + const authService = AuthService.getInstance(); + const tokenManager = TokenManager.getInstance(); + + if (!authService.hasPassword()) { + req.user = { userId: 'default-user' }; + res.locals.authenticated = true; + return next(); + } + + const token = extractToken(req); + + if (token) { + const decoded = tokenManager.verifyToken(token); + if (decoded) { + req.user = { userId: decoded.userId }; + res.locals.authenticated = true; + logger.debug('Authentication successful via token', { userId: decoded.userId }); + return next(); + } + } + + const password = req.query.password || req.body?.password; + + if (password) { + try { + const isValid = await authService.authenticate(password); + if (isValid) { + req.user = { userId: 'default-user' }; + res.locals.authenticated = true; + logger.debug('Authentication successful via password'); + return next(); + } + } catch (error) { + logger.error('Authentication error', { error: error.message }); + } + } + + logger.warn('Authentication failed', { + ip: req.socket?.remoteAddress, + path: req.path, + hasToken: !!token, + hasPassword: !!password + }); + + res.status(401).json({ + error: 'Authentication required', + code: 'AUTH_REQUIRED' + }); +} + +module.exports = authMiddleware; diff --git a/remote/src/middlewares/error.js b/remote/src/middlewares/error.js new file mode 100644 index 0000000..548d41d --- /dev/null +++ b/remote/src/middlewares/error.js @@ -0,0 +1,78 @@ +const logger = require('../utils/logger'); + +function errorHandler(err, req, res, next) { + const statusCode = err.statusCode || err.status || 500; + const errorCode = err.code || 'INTERNAL_ERROR'; + + const errorResponse = { + error: err.message || 'Internal Server Error', + code: errorCode + }; + + if (err.details) { + errorResponse.details = err.details; + } + + if (statusCode >= 500) { + logger.error('Server error', { + error: err.message, + code: errorCode, + stack: err.stack, + path: req.path, + method: req.method, + ip: req.socket?.remoteAddress + }); + } else { + logger.warn('Client error', { + error: err.message, + code: errorCode, + path: req.path, + method: req.method, + ip: req.socket?.remoteAddress + }); + } + + if (process.env.NODE_ENV === 'development' && err.stack) { + errorResponse.stack = err.stack; + } + + res.status(statusCode).json(errorResponse); +} + +class AppError extends Error { + constructor(message, statusCode = 500, code = 'APP_ERROR') { + super(message); + this.statusCode = statusCode; + this.code = code; + this.status = statusCode; + Error.captureStackTrace(this, this.constructor); + } + + withDetails(details) { + this.details = details; + return this; + } +} + +function createError(message, statusCode = 500, code = 'APP_ERROR') { + return new AppError(message, statusCode, code); +} + +function notFoundHandler(req, res, next) { + const error = new AppError(`Not Found - ${req.originalUrl}`, 404, 'NOT_FOUND'); + next(error); +} + +function asyncHandler(fn) { + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} + +module.exports = { + errorHandler, + AppError, + createError, + notFoundHandler, + asyncHandler +}; diff --git a/remote/src/middlewares/rateLimit.js b/remote/src/middlewares/rateLimit.js new file mode 100644 index 0000000..8f65e65 --- /dev/null +++ b/remote/src/middlewares/rateLimit.js @@ -0,0 +1,87 @@ +const logger = require('../utils/logger'); + +class RateLimiter { + constructor(options = {}) { + this.windowMs = options.windowMs || 60 * 1000; + this.maxRequests = options.maxRequests || 5; + this.requests = new Map(); + this.cleanupInterval = setInterval(() => this.cleanup(), this.windowMs); + } + + cleanup() { + const now = Date.now(); + for (const [key, data] of this.requests.entries()) { + if (now - data.startTime > this.windowMs) { + this.requests.delete(key); + } + } + } + + getKey(req) { + return req.ip || req.socket?.remoteAddress || 'unknown'; + } + + middleware() { + return (req, res, next) => { + const key = this.getKey(req); + const now = Date.now(); + + let requestData = this.requests.get(key); + + if (!requestData || now - requestData.startTime > this.windowMs) { + requestData = { + count: 0, + startTime: now + }; + this.requests.set(key, requestData); + } + + requestData.count++; + + const remainingTime = Math.ceil((requestData.startTime + this.windowMs - now) / 1000); + + res.setHeader('X-RateLimit-Limit', this.maxRequests); + res.setHeader('X-RateLimit-Remaining', Math.max(0, this.maxRequests - requestData.count)); + res.setHeader('X-RateLimit-Reset', remainingTime); + + if (requestData.count > this.maxRequests) { + logger.warn('Rate limit exceeded', { + ip: key, + path: req.path, + count: requestData.count, + maxRequests: this.maxRequests + }); + + return res.status(429).json({ + error: 'Too many requests, please try again later', + code: 'RATE_LIMIT_EXCEEDED', + retryAfter: remainingTime + }); + } + + next(); + }; + } + + stop() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } + } +} + +function createRateLimiter(options = {}) { + const limiter = new RateLimiter(options); + return limiter.middleware(); +} + +const defaultRateLimiter = createRateLimiter({ + windowMs: 60 * 1000, + maxRequests: 5 +}); + +module.exports = { + RateLimiter, + createRateLimiter, + defaultRateLimiter +}; diff --git a/remote/src/routes/auth.js b/remote/src/routes/auth.js new file mode 100644 index 0000000..f48d6a2 --- /dev/null +++ b/remote/src/routes/auth.js @@ -0,0 +1,15 @@ +const express = require('express'); +const AuthController = require('../controllers/AuthController'); + +const router = express.Router(); +const authController = new AuthController(); + +router.post('/login', (req, res) => { + authController.login(req, res); +}); + +router.post('/verify', (req, res) => { + authController.verify(req, res); +}); + +module.exports = router; diff --git a/remote/src/routes/files.js b/remote/src/routes/files.js new file mode 100644 index 0000000..11a89ee --- /dev/null +++ b/remote/src/routes/files.js @@ -0,0 +1,135 @@ +const express = require('express'); +const multer = require('multer'); +const crypto = require('crypto'); +const { fileService } = require('../services/file'); +const logger = require('../utils/logger'); + +const router = express.Router(); +const upload = multer({ storage: multer.memoryStorage() }); + +router.get('/', (req, res) => { + try { + const files = fileService.getFileList(); + res.json({ files }); + } catch (error) { + logger.error('Failed to get file list', { error: error.message }); + res.status(500).json({ error: 'Failed to get file list' }); + } +}); + +router.get('/browse', (req, res) => { + try { + const path = req.query.path || ''; + const result = fileService.browseDirectory(path); + res.json(result); + } catch (error) { + logger.error('Failed to browse directory', { error: error.message }); + res.status(500).json({ error: 'Failed to browse directory' }); + } +}); + +router.get('/:filename', (req, res) => { + try { + const filename = req.params.filename; + const range = req.headers.range; + + const result = fileService.getFileStream(filename, range); + + if (!result) { + return res.status(404).json({ error: 'File not found' }); + } + + const headers = { + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes' + }; + + if (range && result.contentRange) { + headers['Content-Range'] = result.contentRange; + headers['Content-Length'] = result.contentLength; + res.writeHead(206, headers); + } else { + headers['Content-Length'] = result.contentLength; + res.writeHead(200, headers); + } + + result.stream.pipe(res); + } catch (error) { + logger.error('Failed to download file', { error: error.message }); + res.status(500).json({ error: 'Failed to download file' }); + } +}); + +router.post('/upload/start', (req, res) => { + try { + const { filename, totalChunks, fileSize } = req.body; + const fileId = crypto.randomBytes(16).toString('hex'); + res.json({ + fileId, + chunkSize: 5 * 1024 * 1024, + message: 'Upload session started' + }); + } catch (error) { + logger.error('Failed to start upload', { error: error.message }); + res.status(500).json({ error: 'Failed to start upload' }); + } +}); + +router.post('/upload/chunk', upload.single('chunk'), (req, res) => { + try { + const { fileId, chunkIndex } = req.body; + + if (!req.file) { + return res.status(400).json({ error: 'No chunk data provided' }); + } + + const success = fileService.saveChunk(fileId, parseInt(chunkIndex), req.file.buffer); + + if (success) { + res.json({ success: true, chunkIndex: parseInt(chunkIndex) }); + } else { + res.status(500).json({ error: 'Failed to save chunk' }); + } + } catch (error) { + logger.error('Failed to upload chunk', { error: error.message }); + res.status(500).json({ error: 'Failed to upload chunk' }); + } +}); + +router.post('/upload/merge', (req, res) => { + try { + const { fileId, totalChunks, filename } = req.body; + + if (!fileId || !totalChunks || !filename) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + const success = fileService.mergeChunks(fileId, parseInt(totalChunks), filename); + + if (success) { + res.json({ success: true, filename }); + } else { + fileService.cleanupChunks(fileId); + res.status(500).json({ error: 'Failed to merge chunks' }); + } + } catch (error) { + logger.error('Failed to merge chunks', { error: error.message }); + res.status(500).json({ error: 'Failed to merge chunks' }); + } +}); + +router.delete('/:filename', (req, res) => { + try { + const success = fileService.deleteFile(req.params.filename); + if (success) { + res.json({ success: true }); + } else { + res.status(404).json({ error: 'File not found' }); + } + } catch (error) { + logger.error('Failed to delete file', { error: error.message }); + res.status(500).json({ error: 'Failed to delete file' }); + } +}); + +module.exports = router; diff --git a/remote/src/routes/index.js b/remote/src/routes/index.js new file mode 100644 index 0000000..4c89691 --- /dev/null +++ b/remote/src/routes/index.js @@ -0,0 +1,18 @@ +const express = require('express'); +const authRoutes = require('./auth'); +const inputRoutes = require('./input'); +const streamRoutes = require('./stream'); +const fileRoutes = require('./files'); + +const router = express.Router(); + +router.use('/auth', authRoutes); +router.use('/input', inputRoutes); +router.use('/stream', streamRoutes); +router.use('/files', fileRoutes); + +router.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +module.exports = router; diff --git a/remote/src/routes/input.js b/remote/src/routes/input.js new file mode 100644 index 0000000..7adbaae --- /dev/null +++ b/remote/src/routes/input.js @@ -0,0 +1,124 @@ +const express = require('express'); +const InputController = require('../controllers/InputController'); + +const router = express.Router(); +const inputController = new InputController(); + +router.post('/mouse/move', async (req, res) => { + const { x, y } = req.body; + if (typeof x !== 'number' || typeof y !== 'number') { + return res.status(400).json({ success: false, error: 'Invalid coordinates' }); + } + try { + await inputController.mouseMove(x, y); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mouse/down', async (req, res) => { + const { button = 'left' } = req.body; + if (!['left', 'right', 'middle'].includes(button)) { + return res.status(400).json({ success: false, error: 'Invalid button' }); + } + try { + await inputController.mouseDown(button); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mouse/up', async (req, res) => { + const { button = 'left' } = req.body; + if (!['left', 'right', 'middle'].includes(button)) { + return res.status(400).json({ success: false, error: 'Invalid button' }); + } + try { + await inputController.mouseUp(button); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mouse/click', async (req, res) => { + const { button = 'left' } = req.body; + if (!['left', 'right', 'middle'].includes(button)) { + return res.status(400).json({ success: false, error: 'Invalid button' }); + } + try { + await inputController.mouseClick(button); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mouse/wheel', async (req, res) => { + const { delta } = req.body; + if (typeof delta !== 'number') { + return res.status(400).json({ success: false, error: 'Invalid delta' }); + } + try { + await inputController.mouseWheel(delta); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/keyboard/down', async (req, res) => { + const { key } = req.body; + if (!key) { + return res.status(400).json({ success: false, error: 'Key is required' }); + } + try { + await inputController.keyDown(key); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/keyboard/up', async (req, res) => { + const { key } = req.body; + if (!key) { + return res.status(400).json({ success: false, error: 'Key is required' }); + } + try { + await inputController.keyUp(key); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/keyboard/press', async (req, res) => { + const { key } = req.body; + if (!key) { + return res.status(400).json({ success: false, error: 'Key is required' }); + } + try { + await inputController.keyPress(key); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/keyboard/type', async (req, res) => { + const { text } = req.body; + if (!text) { + return res.status(400).json({ success: false, error: 'Text is required' }); + } + try { + await inputController.keyType(text); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +module.exports = router; diff --git a/remote/src/routes/stream.js b/remote/src/routes/stream.js new file mode 100644 index 0000000..e93364e --- /dev/null +++ b/remote/src/routes/stream.js @@ -0,0 +1,19 @@ +const express = require('express'); +const StreamController = require('../controllers/StreamController'); + +const router = express.Router(); +const streamController = new StreamController(); + +router.get('/info', (req, res) => { + streamController.getInfo(req, res); +}); + +router.post('/start', (req, res) => { + streamController.start(req, res); +}); + +router.post('/stop', (req, res) => { + streamController.stop(req, res); +}); + +module.exports = router; diff --git a/remote/src/server/InputHandler.js b/remote/src/server/InputHandler.js new file mode 100644 index 0000000..bfa1020 --- /dev/null +++ b/remote/src/server/InputHandler.js @@ -0,0 +1,170 @@ +const logger = require('../utils/logger'); +const MessageTypes = require('./messageTypes'); +const { clipboardService } = require('../services/clipboard'); + +class InputHandler { + constructor(inputService) { + this.inputService = inputService; + this.clipboardService = clipboardService; + this.eventQueue = []; + this.isProcessingQueue = false; + this.lastMouseMove = null; + this.mouseMovePending = false; + } + + handleMessage(message, ws) { + const { type, ...data } = message; + + switch (type) { + case MessageTypes.MOUSE_MOVE: + this.queueMouseMove(data.x, data.y); + break; + case MessageTypes.MOUSE_DOWN: + this.queueEvent('mouseDown', data.button || 'left'); + break; + case MessageTypes.MOUSE_UP: + this.queueEvent('mouseUp', data.button || 'left'); + break; + case MessageTypes.MOUSE_WHEEL: + this.queueEvent('mouseWheel', data.delta); + break; + case MessageTypes.KEY_DOWN: + this.queueEvent('keyDown', data.key); + break; + case MessageTypes.KEY_UP: + this.queueEvent('keyUp', data.key); + break; + case MessageTypes.CLIPBOARD_GET: + this.handleClipboardGet(ws); + break; + case MessageTypes.CLIPBOARD_SET: + this.handleClipboardSet(ws, data); + break; + default: + logger.debug('Unknown message type', { type }); + } + } + + async handleClipboardGet(ws) { + try { + const content = await this.clipboardService.read(); + if (this.clipboardService.isSmallContent(content.size)) { + ws.send(JSON.stringify({ + type: MessageTypes.CLIPBOARD_DATA, + contentType: content.type, + data: content.data, + size: content.size + })); + } else { + ws.send(JSON.stringify({ + type: MessageTypes.CLIPBOARD_TOO_LARGE, + size: content.size + })); + } + } catch (error) { + logger.error('Failed to get clipboard', { error: error.message }); + ws.send(JSON.stringify({ + type: MessageTypes.CLIPBOARD_RESULT, + success: false + })); + } + } + + async handleClipboardSet(ws, data) { + try { + const success = await this.clipboardService.set({ + type: data.contentType, + data: data.data + }); + ws.send(JSON.stringify({ + type: MessageTypes.CLIPBOARD_RESULT, + success + })); + } catch (error) { + logger.error('Failed to set clipboard', { error: error.message }); + ws.send(JSON.stringify({ + type: MessageTypes.CLIPBOARD_RESULT, + success: false + })); + } + } + + queueMouseMove(x, y) { + this.lastMouseMove = { x, y }; + + if (!this.mouseMovePending) { + this.mouseMovePending = true; + setImmediate(() => { + if (this.lastMouseMove) { + this.eventQueue.push({ + type: 'mouseMove', + x: this.lastMouseMove.x, + y: this.lastMouseMove.y + }); + this.mouseMovePending = false; + this.processQueue(); + } + }); + } + } + + queueEvent(type, data) { + this.eventQueue.push({ type, data }); + this.processQueue(); + } + + async processQueue() { + if (this.isProcessingQueue || this.eventQueue.length === 0) { + return; + } + + this.isProcessingQueue = true; + + while (this.eventQueue.length > 0) { + const event = this.eventQueue.shift(); + await this.executeEvent(event); + } + + this.isProcessingQueue = false; + } + + async executeEvent(event) { + try { + switch (event.type) { + case 'mouseMove': + await this.inputService.mouseMove(event.x, event.y); + break; + case 'mouseDown': + await this.inputService.mouseDown(event.data); + break; + case 'mouseUp': + await this.inputService.mouseUp(event.data); + break; + case 'mouseWheel': + await this.inputService.mouseWheel(event.data); + break; + case 'keyDown': + await this.inputService.keyDown(event.data); + break; + case 'keyUp': + await this.inputService.keyUp(event.data); + break; + } + } catch (error) { + logger.error('Failed to execute event', { + type: event.type, + error: error.message + }); + } + } + + stop() { + logger.info('Stopping InputHandler'); + this.eventQueue = []; + this.isProcessingQueue = false; + this.lastMouseMove = null; + this.mouseMovePending = false; + } +} + +module.exports = InputHandler; diff --git a/remote/src/server/Server.js b/remote/src/server/Server.js new file mode 100644 index 0000000..3b7de2e --- /dev/null +++ b/remote/src/server/Server.js @@ -0,0 +1,181 @@ +const express = require('express'); +const http = require('http'); +const path = require('path'); + +class Server { + constructor(config = {}) { + this.port = config.port || 3000; + this.host = config.host || '0.0.0.0'; + this.app = express(); + this.server = http.createServer(this.app); + } + + use(...args) { + this.app.use(...args); + return this; + } + + route(path, router) { + this.app.use(path, router); + return this; + } + + static(staticPath) { + this.app.use(express.static(staticPath)); + return this; + } + + start() { + return new Promise((resolve, reject) => { + this.server.listen({ port: this.port, host: this.host }, () => { + resolve(this.getAddress()); + }); + this.server.on('error', reject); + }); + } + + stop() { + return new Promise((resolve, reject) => { + this.server.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + getAddress() { + const address = this.server.address(); + if (!address) { + return null; + } + return { + port: address.port, + host: this.host, + url: `http://${this.host}:${address.port}` + }; + } + + renderLoginPage(res, errorMsg = '') { + const errorHtml = errorMsg ? `
${errorMsg}
` : ''; + const html = ` + + + + 身份验证 + + + +
+

身份验证

+ ${errorHtml} +
+
+ + +
+
+
+ +`; + res.send(html); + } + + getExpressApp() { + return this.app; + } + + getHTTPServer() { + return this.server; + } +} + +module.exports = Server; diff --git a/remote/src/server/StreamBroadcaster.js b/remote/src/server/StreamBroadcaster.js new file mode 100644 index 0000000..d694a58 --- /dev/null +++ b/remote/src/server/StreamBroadcaster.js @@ -0,0 +1,40 @@ +const logger = require('../utils/logger'); + +class StreamBroadcaster { + constructor(webSocketServer) { + this.wsServer = webSocketServer; + this.encoder = null; + this.dataHandler = null; + } + + setEncoder(encoder) { + this.detachEncoder(); + + this.encoder = encoder; + + if (this.encoder) { + this.dataHandler = (data) => { + const sentCount = this.wsServer.broadcast(data); + logger.debug('Broadcasted frame', { clients: sentCount }); + }; + this.encoder.on('data', this.dataHandler); + logger.info('StreamBroadcaster attached to encoder'); + } + } + + detachEncoder() { + if (this.encoder && this.dataHandler) { + this.encoder.off('data', this.dataHandler); + logger.info('StreamBroadcaster detached from encoder'); + } + this.encoder = null; + this.dataHandler = null; + } + + stop() { + logger.info('Stopping StreamBroadcaster'); + this.detachEncoder(); + } +} + +module.exports = StreamBroadcaster; diff --git a/remote/src/server/WebSocketServer.js b/remote/src/server/WebSocketServer.js new file mode 100644 index 0000000..5d484d7 --- /dev/null +++ b/remote/src/server/WebSocketServer.js @@ -0,0 +1,122 @@ +const WebSocket = require('ws'); +const logger = require('../utils/logger'); +const config = require('../utils/config'); +const MessageTypes = require('./messageTypes'); +const TokenManager = require('../services/auth/TokenManager'); + +class WebSocketServer { + constructor() { + this.wss = null; + this.clients = new Set(); + this.screenWidth = 1280; + this.screenHeight = 720; + } + + setScreenResolution(width, height) { + this.screenWidth = width; + this.screenHeight = height; + } + + start(server) { + this.wss = new WebSocket.Server({ server }); + this.setupConnectionHandler(); + logger.info('WebSocket server started'); + } + + setupConnectionHandler() { + this.wss.on('connection', (ws, req) => { + // 跳过认证检查,允许所有连接(剪贴板同步不需要认证) + const clientIp = req.socket.remoteAddress; + logger.info('Client connected (auth skipped)', { ip: clientIp }); + this.clients.add(ws); + this.sendScreenInfo(ws); + + ws.on('close', () => { + logger.info('Client disconnected', { ip: clientIp }); + this.clients.delete(ws); + }); + + ws.on('error', (error) => { + logger.error('WebSocket error', { error: error.message }); + this.clients.delete(ws); + }); + }); + + this.wss.on('error', (error) => { + logger.error('WebSocket server error', { error: error.message }); + }); + } + + authenticate(ws, req) { + const securityConfig = config.getSecurityConfig(); + const password = securityConfig.password; + + if (!password || password === '') { + return true; + } + + const url = new URL(req.url, `http://${req.headers.host}`); + const providedPassword = url.searchParams.get('password'); + + if (providedPassword === password) { + return true; + } + + if (req.headers.cookie) { + const cookies = req.headers.cookie.split(';').map(c => c.trim()); + const authCookie = cookies.find(c => c.startsWith('auth=')); + if (authCookie) { + const token = decodeURIComponent(authCookie.substring(5)); + const tokenManager = new TokenManager(); + const decoded = tokenManager.verifyToken(token); + if (decoded) { + return true; + } + } + } + + logger.warn('WebSocket authentication failed', { ip: req.socket.remoteAddress }); + ws.close(1008, 'Authentication required'); + return false; + } + + sendScreenInfo(ws) { + ws.send(JSON.stringify({ + type: MessageTypes.SCREEN_INFO, + width: this.screenWidth, + height: this.screenHeight + })); + } + + broadcast(data) { + const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data); + let sentCount = 0; + this.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(buffer, { binary: true }); + sentCount++; + } + }); + return sentCount; + } + + getClientCount() { + return this.clients.size; + } + + stop() { + logger.info('Stopping WebSocket server'); + + this.clients.forEach((client) => { + client.close(); + }); + this.clients.clear(); + + if (this.wss) { + this.wss.close(); + this.wss = null; + } + } +} + +module.exports = WebSocketServer; diff --git a/remote/src/server/messageTypes.js b/remote/src/server/messageTypes.js new file mode 100644 index 0000000..c374f5b --- /dev/null +++ b/remote/src/server/messageTypes.js @@ -0,0 +1,16 @@ +const MessageTypes = { + SCREEN_INFO: 'screenInfo', + MOUSE_MOVE: 'mouseMove', + MOUSE_DOWN: 'mouseDown', + MOUSE_UP: 'mouseUp', + MOUSE_WHEEL: 'mouseWheel', + KEY_DOWN: 'keyDown', + KEY_UP: 'keyUp', + CLIPBOARD_GET: 'clipboardGet', + CLIPBOARD_SET: 'clipboardSet', + CLIPBOARD_DATA: 'clipboardData', + CLIPBOARD_RESULT: 'clipboardResult', + CLIPBOARD_TOO_LARGE: 'clipboardTooLarge' +}; + +module.exports = MessageTypes; diff --git a/remote/src/services/auth/AuthService.js b/remote/src/services/auth/AuthService.js new file mode 100644 index 0000000..c3e489f --- /dev/null +++ b/remote/src/services/auth/AuthService.js @@ -0,0 +1,101 @@ +const bcrypt = require('bcryptjs'); +const logger = require('../../utils/logger'); + +const BCRYPT_COST = 12; +const BCRYPT_HASH_PREFIX = '$2b$'; + +let instance = null; + +class AuthService { + constructor() { + if (instance) { + return instance; + } + + this.passwordHash = null; + this.isHashed = false; + this._initializePassword(); + + instance = this; + } + + _initializePassword() { + const config = require('../../utils/config'); + const securityConfig = config.getSecurityConfig(); + const password = securityConfig.password; + + if (!password) { + logger.warn('No password configured. Authentication will be disabled.'); + return; + } + + if (password.startsWith(BCRYPT_HASH_PREFIX)) { + this.passwordHash = password; + this.isHashed = true; + logger.info('AuthService initialized with bcrypt hash password'); + } else { + this.passwordHash = password; + this.isHashed = false; + logger.info('AuthService initialized with plaintext password'); + } + } + + async hashPassword(password) { + return bcrypt.hash(password, BCRYPT_COST); + } + + async verifyPassword(password, hash) { + return bcrypt.compare(password, hash); + } + + async authenticate(password) { + if (!this.passwordHash) { + logger.debug('Authentication skipped: no password configured'); + return true; + } + + if (!password) { + logger.warn('Authentication failed: no password provided'); + return false; + } + + try { + if (this.isHashed) { + const isValid = await this.verifyPassword(password, this.passwordHash); + if (isValid) { + logger.debug('Authentication successful'); + } else { + logger.warn('Authentication failed: invalid password'); + } + return isValid; + } else { + const hashedInput = await this.hashPassword(password); + const isValid = await this.verifyPassword(password, hashedInput); + + if (password === this.passwordHash) { + logger.debug('Authentication successful'); + return true; + } else { + logger.warn('Authentication failed: invalid password'); + return false; + } + } + } catch (error) { + logger.error('Authentication error', { error: error.message }); + return false; + } + } + + hasPassword() { + return !!this.passwordHash; + } + + static getInstance() { + if (!instance) { + instance = new AuthService(); + } + return instance; + } +} + +module.exports = AuthService; diff --git a/remote/src/services/auth/TokenManager.js b/remote/src/services/auth/TokenManager.js new file mode 100644 index 0000000..9e40f89 --- /dev/null +++ b/remote/src/services/auth/TokenManager.js @@ -0,0 +1,102 @@ +const jwt = require('jsonwebtoken'); +const logger = require('../../utils/logger'); + +const TOKEN_EXPIRY = '24h'; + +let instance = null; + +class TokenManager { + constructor() { + if (instance) { + return instance; + } + + this.secret = this._getSecret(); + + instance = this; + } + + _getSecret() { + const jwtSecret = process.env.JWT_SECRET; + + if (jwtSecret) { + logger.info('TokenManager initialized with JWT_SECRET'); + return jwtSecret; + } + + const password = process.env.REMOTE_SECURITY_PASSWORD; + + if (password) { + logger.info('TokenManager initialized with REMOTE_SECURITY_PASSWORD as secret'); + return password; + } + + const defaultSecret = 'remote-control-default-secret-change-in-production'; + logger.warn('TokenManager using default secret. Please set JWT_SECRET or REMOTE_SECURITY_PASSWORD.'); + return defaultSecret; + } + + generateToken(payload) { + const tokenPayload = { + userId: payload.userId || 'default-user', + iat: Math.floor(Date.now() / 1000) + }; + + const options = { + expiresIn: TOKEN_EXPIRY + }; + + try { + const token = jwt.sign(tokenPayload, this.secret, options); + logger.debug('Token generated', { userId: tokenPayload.userId }); + return token; + } catch (error) { + logger.error('Failed to generate token', { error: error.message }); + return null; + } + } + + verifyToken(token) { + if (!token) { + return null; + } + + try { + const decoded = jwt.verify(token, this.secret); + logger.debug('Token verified', { userId: decoded.userId }); + return decoded; + } catch (error) { + if (error.name === 'TokenExpiredError') { + logger.warn('Token expired', { expiredAt: error.expiredAt }); + } else if (error.name === 'JsonWebTokenError') { + logger.warn('Invalid token', { error: error.message }); + } else { + logger.error('Token verification error', { error: error.message }); + } + return null; + } + } + + decodeToken(token) { + if (!token) { + return null; + } + + try { + const decoded = jwt.decode(token); + return decoded; + } catch (error) { + logger.error('Failed to decode token', { error: error.message }); + return null; + } + } + + static getInstance() { + if (!instance) { + instance = new TokenManager(); + } + return instance; + } +} + +module.exports = TokenManager; diff --git a/remote/src/services/clipboard/ClipboardService.js b/remote/src/services/clipboard/ClipboardService.js new file mode 100644 index 0000000..fde9770 --- /dev/null +++ b/remote/src/services/clipboard/ClipboardService.js @@ -0,0 +1,131 @@ +const { spawn } = require('child_process'); +const logger = require('../../utils/logger'); + +const CLIPBOARD_THRESHOLD = 500 * 1024; + +class ClipboardService { + constructor() { + this.threshold = CLIPBOARD_THRESHOLD; + } + + async read() { + return new Promise((resolve, reject) => { + const psCode = ` +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +if ([Windows.Forms.Clipboard]::ContainsText()) { + $text = [Windows.Forms.Clipboard]::GetText() + Write-Output "TYPE:TEXT" + Write-Output $text +} elseif ([Windows.Forms.Clipboard]::ContainsImage()) { + $image = [Windows.Forms.Clipboard]::GetImage() + if ($image -ne $null) { + $ms = New-Object System.IO.MemoryStream + $image.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) + $bytes = $ms.ToArray() + $base64 = [Convert]::ToBase64String($bytes) + Write-Output "TYPE:IMAGE" + Write-Output $base64 + } +} else { + Write-Output "TYPE:EMPTY" +} +`; + + const ps = spawn('powershell', ['-NoProfile', '-Command', + '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ' + psCode + ], { encoding: 'utf8' }); + let output = ''; + + ps.stdout.on('data', (data) => { + output += data.toString(); + }); + + ps.stderr.on('data', (data) => { + logger.error('Clipboard read error', { error: data.toString() }); + }); + + ps.on('close', () => { + const lines = output.trim().split('\n'); + const typeLine = lines[0]; + + if (typeLine && typeLine.includes('TYPE:TEXT')) { + const text = lines.slice(1).join('\n'); + resolve({ + type: 'text', + data: text, + size: Buffer.byteLength(text, 'utf8') + }); + } else if (typeLine && typeLine.includes('TYPE:IMAGE')) { + const base64 = lines.slice(1).join(''); + const size = Math.ceil(base64.length * 0.75); + resolve({ + type: 'image', + data: base64, + size: size + }); + } else { + resolve({ type: 'empty', data: null, size: 0 }); + } + }); + + ps.on('error', (err) => { + logger.error('Clipboard read failed', { error: err.message }); + resolve({ type: 'empty', data: null, size: 0 }); + }); + }); + } + + async set(content) { + return new Promise((resolve, reject) => { + let psCode; + + if (content.type === 'text') { + const escapedText = content.data + .replace(/'/g, "''") + .replace(/\r\n/g, '`r`n') + .replace(/\n/g, '`n'); + psCode = ` +$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 +Add-Type -AssemblyName System.Windows.Forms +[Windows.Forms.Clipboard]::SetText('${escapedText}') +Write-Output "SUCCESS" +`; + } else if (content.type === 'image') { + psCode = ` +$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +$bytes = [Convert]::FromBase64String('${content.data}') +$ms = New-Object System.IO.MemoryStream(,$bytes) +$image = [System.Drawing.Image]::FromStream($ms) +[Windows.Forms.Clipboard]::SetImage($image) +Write-Output "SUCCESS" +`; + } else { + resolve(false); + return; + } + + const ps = spawn('powershell', ['-NoProfile', '-Command', psCode]); + let output = ''; + + ps.stdout.on('data', (data) => { + output += data.toString(); + }); + + ps.on('close', () => { + resolve(output.includes('SUCCESS')); + }); + + ps.on('error', () => resolve(false)); + }); + } + + isSmallContent(size) { + return size <= this.threshold; + } +} + +module.exports = ClipboardService; diff --git a/remote/src/services/clipboard/index.js b/remote/src/services/clipboard/index.js new file mode 100644 index 0000000..0e86897 --- /dev/null +++ b/remote/src/services/clipboard/index.js @@ -0,0 +1,6 @@ +const ClipboardService = require('./ClipboardService'); + +module.exports = { + ClipboardService, + clipboardService: new ClipboardService() +}; diff --git a/remote/src/services/file/FileService.js b/remote/src/services/file/FileService.js new file mode 100644 index 0000000..0ebdd7f --- /dev/null +++ b/remote/src/services/file/FileService.js @@ -0,0 +1,184 @@ +const fs = require('fs'); +const path = require('path'); +const logger = require('../../utils/logger'); +const paths = require('../../utils/paths'); + +class FileService { + constructor() { + this.uploadDir = paths.getUploadPath(); + this.tempDir = paths.getTempPath(); + this._ensureDirs(); + } + + _ensureDirs() { + if (!fs.existsSync(this.uploadDir)) { + fs.mkdirSync(this.uploadDir, { recursive: true }); + } + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir, { recursive: true }); + } + } + + getFileList() { + try { + const files = fs.readdirSync(this.uploadDir); + return files + .filter(f => { + const filePath = path.join(this.uploadDir, f); + return !fs.statSync(filePath).isDirectory(); + }) + .map(f => { + const filePath = path.join(this.uploadDir, f); + const stat = fs.statSync(filePath); + return { + name: f, + size: stat.size, + modified: stat.mtime, + type: path.extname(f) + }; + }); + } catch (error) { + logger.error('Failed to get file list', { error: error.message }); + return []; + } + } + + getFilePath(filename) { + const filePath = path.join(this.uploadDir, path.basename(filename)); + if (!fs.existsSync(filePath)) { + return null; + } + return filePath; + } + + getFileStream(filename, range) { + const filePath = this.getFilePath(filename); + if (!filePath) return null; + + const stat = fs.statSync(filePath); + const fileSize = stat.size; + + if (range) { + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; + const chunkSize = end - start + 1; + + return { + stream: fs.createReadStream(filePath, { start, end }), + contentRange: `bytes ${start}-${end}/${fileSize}`, + contentLength: chunkSize, + fileSize + }; + } + + return { + stream: fs.createReadStream(filePath), + contentLength: fileSize, + fileSize + }; + } + + saveChunk(fileId, chunkIndex, data) { + try { + const chunkPath = path.join(this.tempDir, `${fileId}.${chunkIndex}`); + fs.writeFileSync(chunkPath, data); + return true; + } catch (error) { + logger.error('Failed to save chunk', { error: error.message }); + return false; + } + } + + mergeChunks(fileId, totalChunks, filename) { + try { + const filePath = path.join(this.uploadDir, path.basename(filename)); + const fd = fs.openSync(filePath, 'w'); + + for (let i = 0; i < totalChunks; i++) { + const chunkPath = path.join(this.tempDir, `${fileId}.${i}`); + if (!fs.existsSync(chunkPath)) { + fs.closeSync(fd); + return false; + } + const chunkData = fs.readFileSync(chunkPath); + fs.writeSync(fd, chunkData, 0, chunkData.length, null); + fs.unlinkSync(chunkPath); + } + + fs.closeSync(fd); + return true; + } catch (error) { + logger.error('Failed to merge chunks', { error: error.message }); + return false; + } + } + + deleteFile(filename) { + const filePath = this.getFilePath(filename); + if (filePath) { + fs.unlinkSync(filePath); + return true; + } + return false; + } + + cleanupChunks(fileId) { + try { + const files = fs.readdirSync(this.tempDir); + files.forEach(f => { + if (f.startsWith(fileId + '.')) { + fs.unlinkSync(path.join(this.tempDir, f)); + } + }); + } catch (error) { + logger.error('Failed to cleanup chunks', { error: error.message }); + } + } + + browseDirectory(relativePath = '') { + try { + const safePath = path.normalize(relativePath || '').replace(/^(\.\.(\/|\\|$))+/, ''); + const targetDir = path.join(this.uploadDir, safePath); + + if (!targetDir.startsWith(this.uploadDir)) { + return { error: 'Access denied', items: [], currentPath: '' }; + } + + if (!fs.existsSync(targetDir)) { + return { error: 'Directory not found', items: [], currentPath: safePath }; + } + + const items = fs.readdirSync(targetDir).map(name => { + const itemPath = path.join(targetDir, name); + const stat = fs.statSync(itemPath); + const isDirectory = stat.isDirectory(); + + return { + name, + isDirectory, + size: isDirectory ? 0 : stat.size, + modified: stat.mtime, + type: isDirectory ? 'directory' : path.extname(name) + }; + }); + + items.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + return { + items, + currentPath: safePath, + parentPath: safePath ? path.dirname(safePath) : null + }; + } catch (error) { + logger.error('Failed to browse directory', { error: error.message }); + return { error: error.message, items: [], currentPath: relativePath }; + } + } +} + +module.exports = FileService; diff --git a/remote/src/services/file/index.js b/remote/src/services/file/index.js new file mode 100644 index 0000000..980a5f7 --- /dev/null +++ b/remote/src/services/file/index.js @@ -0,0 +1,8 @@ +const FileService = require('./FileService'); + +const fileService = new FileService(); + +module.exports = { + FileService, + fileService +}; diff --git a/remote/src/services/index.js b/remote/src/services/index.js new file mode 100644 index 0000000..89c2152 --- /dev/null +++ b/remote/src/services/index.js @@ -0,0 +1,22 @@ +const AuthService = require('./auth/AuthService'); +const TokenManager = require('./auth/TokenManager'); +const { InputService, PowerShellInput } = require('./input'); +const { StreamService, FFmpegEncoder, ScreenCapture } = require('./stream'); +const FRPService = require('./network/FRPService'); +const { ClipboardService, clipboardService } = require('./clipboard'); +const { FileService, fileService } = require('./file'); + +module.exports = { + AuthService, + TokenManager, + InputService, + PowerShellInput, + StreamService, + FFmpegEncoder, + ScreenCapture, + FRPService, + ClipboardService, + clipboardService, + FileService, + fileService +}; diff --git a/remote/src/services/input/InputService.js b/remote/src/services/input/InputService.js new file mode 100644 index 0000000..85fb632 --- /dev/null +++ b/remote/src/services/input/InputService.js @@ -0,0 +1,53 @@ +class InputService { + constructor() { + if (this.constructor === InputService) { + throw new Error('InputService is an abstract class and cannot be instantiated directly'); + } + } + + async mouseMove(x, y) { + throw new Error('Method mouseMove() must be implemented'); + } + + async mouseDown(button = 'left') { + throw new Error('Method mouseDown() must be implemented'); + } + + async mouseUp(button = 'left') { + throw new Error('Method mouseUp() must be implemented'); + } + + async mouseClick(button = 'left') { + throw new Error('Method mouseClick() must be implemented'); + } + + async mouseWheel(delta) { + throw new Error('Method mouseWheel() must be implemented'); + } + + async keyDown(key) { + throw new Error('Method keyDown() must be implemented'); + } + + async keyUp(key) { + throw new Error('Method keyUp() must be implemented'); + } + + async keyPress(key) { + throw new Error('Method keyPress() must be implemented'); + } + + async start() { + throw new Error('Method start() must be implemented'); + } + + async stop() { + throw new Error('Method stop() must be implemented'); + } + + isReady() { + throw new Error('Method isReady() must be implemented'); + } +} + +module.exports = InputService; diff --git a/remote/src/services/input/PowerShellInput.js b/remote/src/services/input/PowerShellInput.js new file mode 100644 index 0000000..8cd6c98 --- /dev/null +++ b/remote/src/services/input/PowerShellInput.js @@ -0,0 +1,275 @@ +const { spawn } = require('child_process'); +const InputService = require('./InputService'); +const logger = require('../../utils/logger'); +const config = require('../../config'); + +const VK_CODES = { + 'enter': 13, 'backspace': 8, 'tab': 9, 'escape': 27, + 'delete': 46, 'home': 36, 'end': 35, 'pageup': 33, + 'pagedown': 34, 'up': 38, 'down': 40, 'left': 37, 'right': 39, + 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, + 'f5': 116, 'f6': 117, 'f7': 118, 'f8': 119, + 'f9': 120, 'f10': 121, 'f11': 122, 'f12': 123, + 'ctrl': 17, 'alt': 18, 'shift': 16, 'win': 91, + 'space': 32, + ',': 188, '.': 190, '/': 191, ';': 186, "'": 222, + '[': 219, ']': 221, '\\': 220, '-': 189, '=': 187, + '`': 192 +}; + +const POWERSHELL_SCRIPT = ` +$env:PSModulePath += ';.' +Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public class Input { + [DllImport("user32.dll")] + public static extern bool SetCursorPos(int X, int Y); + [DllImport("user32.dll")] + public static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo); + [DllImport("user32.dll")] + public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, uint dwExtraInfo); + [DllImport("user32.dll")] + public static extern short GetAsyncKeyState(int vKey); +} +'@ -Language CSharp -ErrorAction SilentlyContinue + +while ($true) { + $line = [Console]::In.ReadLine() + if ($line -eq $null) { break } + if ($line -eq '') { continue } + + try { + $parts = $line -split ' ' + $cmd = $parts[0] + + switch ($cmd) { + 'move' { + $x = [int]$parts[1] + $y = [int]$parts[2] + [Input]::SetCursorPos($x, $y) + } + 'down' { + $btn = $parts[1] + $flag = if ($btn -eq 'right') { 8 } else { 2 } + [Input]::mouse_event($flag, 0, 0, 0, 0) + } + 'up' { + $btn = $parts[1] + $flag = if ($btn -eq 'right') { 16 } else { 4 } + [Input]::mouse_event($flag, 0, 0, 0, 0) + } + 'wheel' { + $delta = [int]$parts[1] + [Input]::mouse_event(2048, 0, 0, $delta, 0) + } + 'kdown' { + $vk = [int]$parts[1] + [Input]::keybd_event([byte]$vk, 0, 0, 0) + } + 'kup' { + $vk = [int]$parts[1] + [Input]::keybd_event([byte]$vk, 0, 2, 0) + } + } + } catch { + Write-Error $_.Exception.Message + } +} +`; + +class PowerShellInput extends InputService { + constructor(options = {}) { + super(); + + this.inputConfig = config.getSection('input') || { + mouseEnabled: true, + keyboardEnabled: true, + sensitivity: 1.0 + }; + + this.mouseEnabled = options.mouseEnabled !== undefined + ? options.mouseEnabled + : this.inputConfig.mouseEnabled; + this.keyboardEnabled = options.keyboardEnabled !== undefined + ? options.keyboardEnabled + : this.inputConfig.keyboardEnabled; + + this.psProcess = null; + this._isReady = false; + this._isStopping = false; + this._restartTimer = null; + } + + _startProcess() { + if (this.psProcess) { + this._stopProcess(); + } + + this.psProcess = spawn('powershell', [ + '-NoProfile', + '-ExecutionPolicy', 'Bypass', + '-Command', POWERSHELL_SCRIPT + ], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + this.psProcess.stdout.on('data', (data) => { + logger.debug('PowerShell stdout', { output: data.toString().trim() }); + }); + + this.psProcess.stderr.on('data', (data) => { + logger.error('PowerShell stderr', { error: data.toString().trim() }); + }); + + this.psProcess.on('close', (code) => { + logger.warn('PowerShell process closed', { code }); + this._isReady = false; + this.psProcess = null; + this._scheduleRestart(); + }); + + this.psProcess.on('error', (error) => { + logger.error('PowerShell process error', { error: error.message }); + this._isReady = false; + }); + + this._isReady = true; + logger.info('PowerShellInput process started'); + } + + _stopProcess() { + if (this.psProcess) { + this.psProcess.kill(); + this.psProcess = null; + this._isReady = false; + } + + if (this._restartTimer) { + clearTimeout(this._restartTimer); + this._restartTimer = null; + } + } + + _scheduleRestart() { + if (this._isStopping) { + return; + } + + if (this._restartTimer) { + clearTimeout(this._restartTimer); + } + + this._restartTimer = setTimeout(() => { + if (!this._isStopping && (this.mouseEnabled || this.keyboardEnabled)) { + logger.info('Restarting PowerShell process after crash'); + this._startProcess(); + } + }, 1000); + } + + async _sendCommand(cmd) { + if (!this._isReady || !this.psProcess) { + return; + } + + return new Promise((resolve, reject) => { + try { + this.psProcess.stdin.write(cmd + '\n'); + resolve(); + } catch (error) { + logger.error('Failed to send command', { cmd, error: error.message }); + reject(error); + } + }); + } + + _getVkCode(key) { + const lowerKey = key.toLowerCase(); + if (VK_CODES[lowerKey]) { + return VK_CODES[lowerKey]; + } + + if (key.length === 1) { + if (VK_CODES[key]) { + return VK_CODES[key]; + } + return key.toUpperCase().charCodeAt(0); + } + + return null; + } + + async mouseMove(x, y) { + if (!this.mouseEnabled) return; + await this._sendCommand(`move ${Math.floor(x)} ${Math.floor(y)}`); + } + + async mouseDown(button = 'left') { + if (!this.mouseEnabled) return; + await this._sendCommand(`down ${button}`); + } + + async mouseUp(button = 'left') { + if (!this.mouseEnabled) return; + await this._sendCommand(`up ${button}`); + } + + async mouseClick(button = 'left') { + if (!this.mouseEnabled) return; + await this.mouseDown(button); + await new Promise(r => setTimeout(r, 10)); + await this.mouseUp(button); + } + + async mouseWheel(delta) { + if (!this.mouseEnabled) return; + await this._sendCommand(`wheel ${Math.floor(delta)}`); + } + + async keyDown(key) { + if (!this.keyboardEnabled) return; + const vk = this._getVkCode(key); + if (vk) { + await this._sendCommand(`kdown ${vk}`); + } + } + + async keyUp(key) { + if (!this.keyboardEnabled) return; + const vk = this._getVkCode(key); + if (vk) { + await this._sendCommand(`kup ${vk}`); + } + } + + async keyPress(key) { + if (!this.keyboardEnabled) return; + await this.keyDown(key); + await new Promise(r => setTimeout(r, 10)); + await this.keyUp(key); + } + + async start() { + if (this.mouseEnabled || this.keyboardEnabled) { + this._isStopping = false; + this._startProcess(); + } + logger.info('PowerShellInput started', { + mouseEnabled: this.mouseEnabled, + keyboardEnabled: this.keyboardEnabled + }); + } + + async stop() { + this._isStopping = true; + this._stopProcess(); + logger.info('PowerShellInput stopped'); + } + + isReady() { + return this._isReady; + } +} + +module.exports = PowerShellInput; diff --git a/remote/src/services/input/index.js b/remote/src/services/input/index.js new file mode 100644 index 0000000..3fc7e6f --- /dev/null +++ b/remote/src/services/input/index.js @@ -0,0 +1,7 @@ +const InputService = require('./InputService'); +const PowerShellInput = require('./PowerShellInput'); + +module.exports = { + InputService, + PowerShellInput +}; diff --git a/remote/src/services/network/FRPService.js b/remote/src/services/network/FRPService.js new file mode 100644 index 0000000..65ede32 --- /dev/null +++ b/remote/src/services/network/FRPService.js @@ -0,0 +1,131 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const config = require('../../config'); +const paths = require('../../utils/paths'); +const logger = require('../../utils/logger'); + +class FRPService { + constructor(options = {}) { + this.enabled = options.enabled !== false; + this.frpcPath = options.frpcPath || path.join(paths.getFRPPath(), 'frpc.exe'); + this.configPath = options.configPath || path.join(paths.getFRPPath(), 'frpc.toml'); + this.process = null; + this.isRunning = false; + } + + _prepareConfig() { + const frpDir = paths.getFRPPath(); + const logPath = path.join(paths.getBasePath(), 'logs', 'frpc.log'); + const logsDir = path.dirname(logPath); + + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + if (fs.existsSync(this.configPath)) { + let content = fs.readFileSync(this.configPath, 'utf8'); + content = content.replace(/log\.to\s*=\s*"[^"]*"/, `log.to = "${logPath.replace(/\\/g, '\\\\')}"`); + + const tempConfigPath = path.join(frpDir, 'frpc-runtime.toml'); + fs.writeFileSync(tempConfigPath, content); + return tempConfigPath; + } + + return this.configPath; + } + + start() { + if (!this.enabled) { + logger.info('FRP service is disabled'); + return; + } + + if (this.isRunning) { + logger.warn('FRP service is already running'); + return; + } + + try { + if (!fs.existsSync(this.frpcPath)) { + logger.error('FRP client not found', { path: this.frpcPath }); + return; + } + + if (!fs.existsSync(this.configPath)) { + logger.error('FRP config not found', { path: this.configPath }); + return; + } + + const runtimeConfigPath = this._prepareConfig(); + + logger.info('Starting FRP client', { + frpcPath: this.frpcPath, + configPath: runtimeConfigPath + }); + + this.process = spawn(this.frpcPath, ['-c', runtimeConfigPath], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + }); + + this.isRunning = true; + + this.process.stdout.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.info(`[FRP] ${output}`); + } + }); + + this.process.stderr.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.error(`[FRP] ${output}`); + } + }); + + this.process.on('error', (error) => { + logger.error('FRP process error', { error: error.message }); + this.isRunning = false; + }); + + this.process.on('close', (code) => { + logger.info('FRP process closed', { code }); + this.isRunning = false; + this.process = null; + }); + + logger.info('FRP service started successfully'); + } catch (error) { + logger.error('Failed to start FRP service', { error: error.message }); + this.isRunning = false; + } + } + + stop() { + if (!this.isRunning || !this.process) { + return; + } + + logger.info('Stopping FRP service'); + + try { + this.process.kill(); + this.process = null; + this.isRunning = false; + logger.info('FRP service stopped'); + } catch (error) { + logger.error('Failed to stop FRP service', { error: error.message }); + } + } + + getStatus() { + return { + enabled: this.enabled, + running: this.isRunning + }; + } +} + +module.exports = FRPService; diff --git a/remote/src/services/network/GiteaService.js b/remote/src/services/network/GiteaService.js new file mode 100644 index 0000000..094d48f --- /dev/null +++ b/remote/src/services/network/GiteaService.js @@ -0,0 +1,107 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const logger = require('../../utils/logger'); +const paths = require('../../utils/paths'); + +class GiteaService { + constructor(options = {}) { + this.enabled = options.enabled !== false; + this.giteaPath = options.giteaPath || path.join(paths.getBasePath(), 'gitea', 'gitea.exe'); + this.workPath = options.workPath || path.join(paths.getBasePath(), 'gitea'); + this.process = null; + this.isRunning = false; + } + + start() { + if (!this.enabled) { + logger.info('Gitea service is disabled'); + return; + } + + if (this.isRunning) { + logger.warn('Gitea service is already running'); + return; + } + + try { + if (!fs.existsSync(this.giteaPath)) { + logger.error('Gitea executable not found', { path: this.giteaPath }); + return; + } + + logger.info('Starting Gitea service', { + giteaPath: this.giteaPath, + workPath: this.workPath + }); + + this.process = spawn(this.giteaPath, ['web'], { + cwd: this.workPath, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + env: { + ...process.env, + GITEA_WORK_DIR: this.workPath + } + }); + + this.isRunning = true; + + this.process.stdout.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.info(`[Gitea] ${output}`); + } + }); + + this.process.stderr.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + logger.error(`[Gitea] ${output}`); + } + }); + + this.process.on('error', (error) => { + logger.error('Gitea process error', { error: error.message }); + this.isRunning = false; + }); + + this.process.on('close', (code) => { + logger.info('Gitea process closed', { code }); + this.isRunning = false; + this.process = null; + }); + + logger.info('Gitea service started successfully'); + } catch (error) { + logger.error('Failed to start Gitea service', { error: error.message }); + this.isRunning = false; + } + } + + stop() { + if (!this.isRunning || !this.process) { + return; + } + + logger.info('Stopping Gitea service'); + + try { + this.process.kill(); + this.process = null; + this.isRunning = false; + logger.info('Gitea service stopped'); + } catch (error) { + logger.error('Failed to stop Gitea service', { error: error.message }); + } + } + + getStatus() { + return { + enabled: this.enabled, + running: this.isRunning + }; + } +} + +module.exports = GiteaService; diff --git a/remote/src/services/stream/FFmpegEncoder.js b/remote/src/services/stream/FFmpegEncoder.js new file mode 100644 index 0000000..c00014e --- /dev/null +++ b/remote/src/services/stream/FFmpegEncoder.js @@ -0,0 +1,191 @@ +const { spawn, execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const StreamService = require('./StreamService'); +const logger = require('../../utils/logger'); +const config = require('../../utils/config'); +const paths = require('../../utils/paths'); + +function getFFmpegPath() { + if (process.pkg) { + const externalPath = path.join(paths.getBasePath(), 'ffmpeg.exe'); + if (fs.existsSync(externalPath)) { + return externalPath; + } + const bundledPath = path.join(paths.getBasePath(), 'bin', 'ffmpeg.exe'); + if (fs.existsSync(bundledPath)) { + return bundledPath; + } + } + return require('@ffmpeg-installer/ffmpeg').path; +} + +const ffmpegPath = getFFmpegPath(); + +class FFmpegEncoder extends StreamService { + constructor() { + super(); + const streamConfig = config.getSection('stream') || {}; + this.fps = streamConfig.fps || 30; + this.bitrate = streamConfig.bitrate || '2M'; + this.gop = streamConfig.gop || 30; + this.resolution = streamConfig.resolution || { width: 1920, height: 1080 }; + this.ffmpegProcess = null; + this.running = false; + this.screenWidth = 1280; + this.screenHeight = 720; + this.retryCount = 0; + this.maxRetries = 3; + this.retryDelay = 5000; + this.retryTimer = null; + this._getScreenResolution(); + } + + _getScreenResolution() { + try { + const output = execSync( + 'powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height"', + { encoding: 'utf8' } + ); + const lines = output.trim().split('\n'); + if (lines.length >= 2) { + this.screenWidth = parseInt(lines[0].trim(), 10); + this.screenHeight = parseInt(lines[1].trim(), 10); + logger.info('Detected screen resolution', { width: this.screenWidth, height: this.screenHeight }); + } + } catch (error) { + logger.warn('Failed to get screen resolution, using defaults', { error: error.message }); + } + } + + getScreenResolution() { + return { width: this.screenWidth, height: this.screenHeight }; + } + + _getArgs() { + const targetWidth = this.resolution.width; + const targetHeight = this.resolution.height; + + const args = [ + '-hide_banner', + '-loglevel', 'error', + '-f', 'gdigrab', + '-framerate', String(this.fps), + '-i', 'desktop', + '-vf', `scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease`, + '-c:v', 'mpeg1video', + '-b:v', this.bitrate, + '-bf', '0', + '-g', String(this.gop), + '-r', String(this.fps), + '-f', 'mpegts', + '-flush_packets', '1', + 'pipe:1' + ]; + + return args; + } + + start() { + if (this.running) { + return; + } + + this.running = true; + this.retryCount = 0; + this._startProcess(); + } + + _startProcess() { + const args = this._getArgs(); + + logger.info('Starting FFmpeg encoder', { + fps: this.fps, + bitrate: this.bitrate, + attempt: this.retryCount + 1 + }); + + this.ffmpegProcess = spawn(ffmpegPath, args); + this.emit('start'); + + this.ffmpegProcess.stderr.on('data', (data) => { + const msg = data.toString().trim(); + if (msg) { + logger.debug('FFmpeg stderr', { message: msg }); + } + }); + + this.ffmpegProcess.stdout.on('data', (chunk) => { + this.emit('data', chunk); + }); + + this.ffmpegProcess.on('error', (err) => { + logger.error('FFmpeg process error', { error: err.message }); + this.emit('error', err); + }); + + this.ffmpegProcess.on('close', (code) => { + logger.info('FFmpeg process closed', { code }); + this.ffmpegProcess = null; + + if (code !== 0 && code !== null && this.running) { + this._handleCrash(); + } else if (this.running) { + this.running = false; + this.emit('stop'); + } + }); + } + + _handleCrash() { + this.retryCount++; + + if (this.retryCount <= this.maxRetries) { + logger.warn(`FFmpeg crashed, retrying (${this.retryCount}/${this.maxRetries}) in ${this.retryDelay / 1000}s`); + + this.emit('error', new Error(`FFmpeg crashed, retry attempt ${this.retryCount}/${this.maxRetries}`)); + + this.retryTimer = setTimeout(() => { + if (this.running) { + this._startProcess(); + } + }, this.retryDelay); + } else { + logger.error('FFmpeg max retries exceeded, stopping'); + this.running = false; + this.emit('error', new Error('FFmpeg max retries exceeded')); + this.emit('stop'); + } + } + + stop() { + if (!this.running) { + return; + } + + logger.info('Stopping FFmpeg encoder'); + this.running = false; + + if (this.retryTimer) { + clearTimeout(this.retryTimer); + this.retryTimer = null; + } + + if (this.ffmpegProcess) { + this.ffmpegProcess.kill('SIGTERM'); + this.ffmpegProcess = null; + } + + this.emit('stop'); + } + + isRunning() { + return this.running; + } + + getEncoder() { + return 'mpeg1video'; + } +} + +module.exports = FFmpegEncoder; diff --git a/remote/src/services/stream/ScreenCapture.js b/remote/src/services/stream/ScreenCapture.js new file mode 100644 index 0000000..34f18e8 --- /dev/null +++ b/remote/src/services/stream/ScreenCapture.js @@ -0,0 +1,78 @@ +const StreamService = require('./StreamService'); +const screenshot = require('screenshot-desktop'); +const sharp = require('sharp'); + +class ScreenCapture extends StreamService { + constructor(options = {}) { + super(); + this.fps = options.fps || 10; + this.quality = options.quality || 80; + this.maxRetries = options.maxRetries || 10; + this.interval = null; + this.running = false; + this.consecutiveErrors = 0; + } + + start() { + if (this.running) { + return; + } + this.running = true; + this.consecutiveErrors = 0; + this.emit('start'); + + const frameInterval = Math.floor(1000 / this.fps); + this.interval = setInterval(async () => { + try { + const buffer = await this.captureFrame(); + if (buffer) { + this.consecutiveErrors = 0; + this.emit('data', buffer); + } + } catch (error) { + this.consecutiveErrors++; + console.error(`Capture error (${this.consecutiveErrors}/${this.maxRetries}):`, error.message); + if (this.consecutiveErrors >= this.maxRetries) { + this.emit('error', error); + this.stop(); + } + } + }, frameInterval); + } + + stop() { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + this.running = false; + this.consecutiveErrors = 0; + this.emit('stop'); + } + + async captureFrame() { + const rawBuffer = await screenshot({ format: 'png' }); + const compressedBuffer = await sharp(rawBuffer) + .jpeg({ quality: this.quality }) + .toBuffer(); + return compressedBuffer; + } + + isRunning() { + return this.running; + } + + setFps(fps) { + this.fps = fps; + if (this.running) { + this.stop(); + this.start(); + } + } + + setQuality(quality) { + this.quality = quality; + } +} + +module.exports = ScreenCapture; diff --git a/remote/src/services/stream/StreamService.js b/remote/src/services/stream/StreamService.js new file mode 100644 index 0000000..037d309 --- /dev/null +++ b/remote/src/services/stream/StreamService.js @@ -0,0 +1,44 @@ +const { EventEmitter } = require('events'); + +class StreamService extends EventEmitter { + constructor() { + super(); + if (this.constructor === StreamService) { + throw new Error('StreamService is an abstract class and cannot be instantiated directly'); + } + } + + start() { + throw new Error('Method start() must be implemented by subclass'); + } + + stop() { + throw new Error('Method stop() must be implemented by subclass'); + } + + isRunning() { + throw new Error('Method isRunning() must be implemented by subclass'); + } + + onFrame(callback) { + this.on('data', callback); + return this; + } + + onError(callback) { + this.on('error', callback); + return this; + } + + onStart(callback) { + this.on('start', callback); + return this; + } + + onStop(callback) { + this.on('stop', callback); + return this; + } +} + +module.exports = StreamService; diff --git a/remote/src/services/stream/index.js b/remote/src/services/stream/index.js new file mode 100644 index 0000000..54455c0 --- /dev/null +++ b/remote/src/services/stream/index.js @@ -0,0 +1,9 @@ +const StreamService = require('./StreamService'); +const FFmpegEncoder = require('./FFmpegEncoder'); +const ScreenCapture = require('./ScreenCapture'); + +module.exports = { + StreamService, + FFmpegEncoder, + ScreenCapture +}; diff --git a/remote/src/utils/config.js b/remote/src/utils/config.js new file mode 100644 index 0000000..1d3aa43 --- /dev/null +++ b/remote/src/utils/config.js @@ -0,0 +1,38 @@ +const newConfig = require('../config'); + +module.exports = { + getServerConfig() { + return newConfig.getSection('server') || {}; + }, + + getStreamConfig() { + return newConfig.getSection('stream') || {}; + }, + + getInputConfig() { + return newConfig.getSection('input') || {}; + }, + + getSecurityConfig() { + return newConfig.getSection('security') || {}; + }, + + getFRPConfig() { + return newConfig.getSection('frp') || { enabled: false }; + }, + + getConfig() { + return newConfig.getAll(); + }, + + setConfig(newConfig) { + return this.getConfig(); + }, + + get: newConfig.get, + getSection: newConfig.getSection, + getAll: newConfig.getAll, + reload: newConfig.reload, + clearCache: newConfig.clearCache, + validate: newConfig.validate +}; diff --git a/remote/src/utils/logger.js b/remote/src/utils/logger.js new file mode 100644 index 0000000..f5a3f01 --- /dev/null +++ b/remote/src/utils/logger.js @@ -0,0 +1,46 @@ +const winston = require('winston'); +const path = require('path'); +const fs = require('fs'); +const paths = require('./paths'); + +const logDir = path.join(paths.getBasePath(), 'logs'); + +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); +} + +const baseLogger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), + winston.format.json() + ), + defaultMeta: { service: 'remote-screen' }, + transports: [ + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), + level: 'error' + }), + new winston.transports.File({ + filename: path.join(logDir, 'combined.log') + }) + ] +}); + +if (process.env.NODE_ENV !== 'production' && !process.pkg) { + baseLogger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + })); +} + +function createLogger(moduleName) { + return baseLogger.child({ module: moduleName }); +} + +module.exports = baseLogger; +module.exports.createLogger = createLogger; diff --git a/remote/src/utils/paths.js b/remote/src/utils/paths.js new file mode 100644 index 0000000..4536ece --- /dev/null +++ b/remote/src/utils/paths.js @@ -0,0 +1,43 @@ +const path = require('path'); +const fs = require('fs'); + +function getBasePath() { + if (process.pkg) { + return path.dirname(process.execPath); + } + return path.join(__dirname, '../..'); +} + +function getPublicPath() { + return path.join(getBasePath(), 'public'); +} + +function getConfigPath() { + return path.join(getBasePath(), 'config'); +} + +function getFRPPath() { + return path.join(getBasePath(), 'frp'); +} + +function getUploadPath() { + return path.join(getBasePath(), 'uploads'); +} + +function getTempPath() { + return path.join(getBasePath(), 'uploads', 'temp'); +} + +function resourceExists(resourcePath) { + return fs.existsSync(resourcePath); +} + +module.exports = { + getBasePath, + getPublicPath, + getConfigPath, + getFRPPath, + getUploadPath, + getTempPath, + resourceExists +}; diff --git a/shared/constants/api.ts b/shared/constants/api.ts new file mode 100644 index 0000000..4fd4e1b --- /dev/null +++ b/shared/constants/api.ts @@ -0,0 +1,37 @@ +export const API_ENDPOINTS = { + FILES: '/api/files', + FILES_CONTENT: '/api/files/content', + FILES_MOVE: '/api/files/move', + FILES_RECYCLE: '/api/files/recycle', + FILES_EXISTS: '/api/files/exists', + FILES_RENAME: '/api/files/rename', + FILES_CREATE: '/api/files/create', + FILES_MKDIR: '/api/files/mkdir', + FILES_RESTORE: '/api/files/restore', + FILES_PERMANENT_DELETE: '/api/files/permanent-delete', + FILES_EMPTY_RECYCLE_BIN: '/api/files/empty-recycle-bin', + TIME: '/api/time', + TIME_EVENT: '/api/time/event', + TIME_STATS: '/api/time/stats', + TIME_DAY: '/api/time/day', + TIME_MONTH: '/api/time/month', + TIME_YEAR: '/api/time/year', + TODO: '/api/todo', + TODO_ADD: '/api/todo/add', + TODO_TOGGLE: '/api/todo/toggle', + TODO_UPDATE: '/api/todo/update', + TODO_DELETE: '/api/todo/delete', + TODO_MIGRATE: '/api/todo/migrate', + SEARCH: '/api/search', + EVENTS: '/api/events', + SYNC: '/api/sync', + SYNC_UPLOAD: '/api/sync/upload', + SYNC_DOWNLOAD: '/api/sync/download', + SYNC_STATUS: '/api/sync/status', + PYDEMOS: '/api/pydemos', + PYDEMOS_CREATE: '/api/pydemos/create', + PYDEMOS_RENAME: '/api/pydemos/rename', + PYDEMOS_DELETE: '/api/pydemos/delete', +} as const + +export type ApiEndpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS] diff --git a/shared/constants/asyncImport.ts b/shared/constants/asyncImport.ts new file mode 100644 index 0000000..d77cca0 --- /dev/null +++ b/shared/constants/asyncImport.ts @@ -0,0 +1,14 @@ +export const ASYNC_IMPORT_STATUS = { + PDF_PARSING_TITLE: '# 正在解析 PDF 内容...', + HTML_PARSING_TITLE: '# 正在解析 HTML 内容...', + PDF_PARSING_CONTENT: '# 正在解析 PDF 内容...\n\n> 请稍候,系统正在后台解析您的 PDF 文件。解析完成后,本文档将自动更新。\n> \n> 您可以继续进行其他操作。', + HTML_PARSING_CONTENT: '# 正在解析 HTML 内容...\n\n> 请稍候,系统正在解析您的 HTML 文件。解析完成后,本文档将自动更新。', +} as const + +export const isAsyncImportProcessingContent = (text: string): boolean => { + const trimmed = text.trimStart() + return ( + trimmed.startsWith(ASYNC_IMPORT_STATUS.PDF_PARSING_TITLE) || + trimmed.startsWith(ASYNC_IMPORT_STATUS.HTML_PARSING_TITLE) + ) +} diff --git a/shared/constants/dates.ts b/shared/constants/dates.ts new file mode 100644 index 0000000..6d0931a --- /dev/null +++ b/shared/constants/dates.ts @@ -0,0 +1 @@ +export const WEEK_DAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const diff --git a/shared/constants/errors.ts b/shared/constants/errors.ts new file mode 100644 index 0000000..fc9d9ac --- /dev/null +++ b/shared/constants/errors.ts @@ -0,0 +1,22 @@ +export const ERROR_CODES = { + PATH_NOT_FOUND: 'PATH_NOT_FOUND', + NOT_A_DIRECTORY: 'NOT_A_DIRECTORY', + ACCESS_DENIED: 'ACCESS_DENIED', + FILE_EXISTS: 'FILE_EXISTS', + INVALID_PATH: 'INVALID_PATH', + VALIDATION_ERROR: 'VALIDATION_ERROR', + INTERNAL_ERROR: 'INTERNAL_ERROR', + NOT_FOUND: 'NOT_FOUND', + BAD_REQUEST: 'BAD_REQUEST', + NAME_GENERATION_FAILED: 'NAME_GENERATION_FAILED', + SSE_UNSUPPORTED: 'SSE_UNSUPPORTED', + ALREADY_EXISTS: 'ALREADY_EXISTS', + NOT_A_FILE: 'NOT_A_FILE', + FORBIDDEN: 'FORBIDDEN', + UNSUPPORTED_MEDIA_TYPE: 'UNSUPPORTED_MEDIA_TYPE', + PAYLOAD_TOO_LARGE: 'PAYLOAD_TOO_LARGE', + RESOURCE_LOCKED: 'RESOURCE_LOCKED', + INVALID_NAME: 'INVALID_NAME', +} as const + +export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES] diff --git a/shared/constants/index.ts b/shared/constants/index.ts new file mode 100644 index 0000000..a491dbf --- /dev/null +++ b/shared/constants/index.ts @@ -0,0 +1,5 @@ +export { API_ENDPOINTS, type ApiEndpoint } from './api.js' +export { ERROR_CODES, type ErrorCode } from './errors.js' +export { ERROR_MESSAGES, getErrorMessage } from './messages.js' +export { ASYNC_IMPORT_STATUS, isAsyncImportProcessingContent } from './asyncImport.js' +export { WEEK_DAYS } from './dates.js' diff --git a/shared/constants/messages.ts b/shared/constants/messages.ts new file mode 100644 index 0000000..10a9323 --- /dev/null +++ b/shared/constants/messages.ts @@ -0,0 +1,26 @@ +import { ERROR_CODES } from './errors' + +export const ERROR_MESSAGES: Record = { + [ERROR_CODES.PATH_NOT_FOUND]: '路径不存在', + [ERROR_CODES.NOT_A_DIRECTORY]: '不是目录', + [ERROR_CODES.ACCESS_DENIED]: '访问被拒绝', + [ERROR_CODES.FILE_EXISTS]: '文件已存在', + [ERROR_CODES.INVALID_PATH]: '无效路径', + [ERROR_CODES.VALIDATION_ERROR]: '验证失败', + [ERROR_CODES.INTERNAL_ERROR]: '内部错误', + [ERROR_CODES.NOT_FOUND]: '资源不存在', + [ERROR_CODES.BAD_REQUEST]: '请求错误', + [ERROR_CODES.NAME_GENERATION_FAILED]: '名称生成失败', + [ERROR_CODES.SSE_UNSUPPORTED]: 'SSE 不支持', + [ERROR_CODES.ALREADY_EXISTS]: '已存在', + [ERROR_CODES.NOT_A_FILE]: '不是文件', + [ERROR_CODES.FORBIDDEN]: '禁止访问', + [ERROR_CODES.UNSUPPORTED_MEDIA_TYPE]: '不支持的媒体类型', + [ERROR_CODES.PAYLOAD_TOO_LARGE]: '请求体过大', + [ERROR_CODES.RESOURCE_LOCKED]: '资源已锁定', + [ERROR_CODES.INVALID_NAME]: '无效名称', +} + +export const getErrorMessage = (code: string): string => { + return ERROR_MESSAGES[code] || '未知错误' +} diff --git a/shared/errors/index.ts b/shared/errors/index.ts new file mode 100644 index 0000000..4b1b450 --- /dev/null +++ b/shared/errors/index.ts @@ -0,0 +1,171 @@ +import type { ErrorCode } from '../constants/errors.js' + +export type { ErrorCode } from '../constants/errors.js' + +export interface ErrorDetails { + [key: string]: unknown +} + +export class AppError extends Error { + public readonly statusCode: number + public readonly details?: ErrorDetails + + constructor( + public readonly code: ErrorCode, + message: string, + statusCode: number = 500, + details?: ErrorDetails + ) { + super(message) + this.name = 'AppError' + this.statusCode = statusCode + this.details = details + } + + toJSON() { + return { + name: this.name, + code: this.code, + message: this.message, + statusCode: this.statusCode, + details: this.details + } + } +} + +export class ValidationError extends AppError { + constructor(message: string, details?: ErrorDetails) { + super('VALIDATION_ERROR', message, 400, details) + this.name = 'ValidationError' + } +} + +export class NotFoundError extends AppError { + constructor(message: string = 'Resource not found', details?: ErrorDetails) { + super('NOT_FOUND', message, 404, details) + this.name = 'NotFoundError' + } +} + +export class AccessDeniedError extends AppError { + constructor(message: string = 'Access denied', details?: ErrorDetails) { + super('ACCESS_DENIED', message, 403, details) + this.name = 'AccessDeniedError' + } +} + +export class BadRequestError extends AppError { + constructor(message: string, details?: ErrorDetails) { + super('BAD_REQUEST', message, 400, details) + this.name = 'BadRequestError' + } +} + +export class PathNotFoundError extends AppError { + constructor(message: string = 'Path not found', details?: ErrorDetails) { + super('PATH_NOT_FOUND', message, 404, details) + this.name = 'PathNotFoundError' + } +} + +export class NotADirectoryError extends AppError { + constructor(message: string = '不是目录', details?: ErrorDetails) { + super('NOT_A_DIRECTORY', message, 400, details) + this.name = 'NotADirectoryError' + } +} + +export class FileExistsError extends AppError { + constructor(message: string = 'File already exists', details?: ErrorDetails) { + super('FILE_EXISTS', message, 409, details) + this.name = 'FileExistsError' + } +} + +export class InvalidPathError extends AppError { + constructor(message: string = '无效路径', details?: ErrorDetails) { + super('INVALID_PATH', message, 400, details) + this.name = 'InvalidPathError' + } +} + +export class AlreadyExistsError extends AppError { + constructor(message: string = 'Resource already exists', details?: ErrorDetails) { + super('ALREADY_EXISTS', message, 409, details) + this.name = 'AlreadyExistsError' + } +} + +export class ForbiddenError extends AppError { + constructor(message: string = '禁止访问', details?: ErrorDetails) { + super('FORBIDDEN', message, 403, details) + this.name = 'ForbiddenError' + } +} + +export class UnsupportedMediaTypeError extends AppError { + constructor(message: string = '不支持的媒体类型', details?: ErrorDetails) { + super('UNSUPPORTED_MEDIA_TYPE', message, 415, details) + this.name = 'UnsupportedMediaTypeError' + } +} + +export class PayloadTooLargeError extends AppError { + constructor(message: string = 'Payload too large', details?: ErrorDetails) { + super('PAYLOAD_TOO_LARGE', message, 413, details) + this.name = 'PayloadTooLargeError' + } +} + +export class ResourceLockedError extends AppError { + constructor(message: string = '资源已锁定', details?: ErrorDetails) { + super('RESOURCE_LOCKED', message, 423, details) + this.name = 'ResourceLockedError' + } +} + +export class InternalError extends AppError { + constructor(message: string = '服务器内部错误', details?: ErrorDetails) { + super('INTERNAL_ERROR', message, 500, details) + this.name = 'InternalError' + } +} + +export function isAppError(error: unknown): error is AppError { + return error instanceof AppError +} + +export function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error +} + +export function createErrorFromCode(code: ErrorCode, message: string, details?: ErrorDetails): AppError { + switch (code) { + case 'VALIDATION_ERROR': + return new ValidationError(message, details) + case 'NOT_FOUND': + case 'PATH_NOT_FOUND': + return new NotFoundError(message, details) + case 'ACCESS_DENIED': + case 'FORBIDDEN': + return new AccessDeniedError(message, details) + case 'BAD_REQUEST': + case 'NOT_A_DIRECTORY': + case 'INVALID_PATH': + case 'INVALID_NAME': + return new BadRequestError(message, details) + case 'FILE_EXISTS': + case 'ALREADY_EXISTS': + return new FileExistsError(message, details) + case 'UNSUPPORTED_MEDIA_TYPE': + return new UnsupportedMediaTypeError(message, details) + case 'PAYLOAD_TOO_LARGE': + return new PayloadTooLargeError(message, details) + case 'RESOURCE_LOCKED': + return new ResourceLockedError(message, details) + case 'INTERNAL_ERROR': + return new InternalError(message, details) + default: + return new AppError(code, message, 500, details) + } +} diff --git a/shared/index.ts b/shared/index.ts new file mode 100644 index 0000000..3249072 --- /dev/null +++ b/shared/index.ts @@ -0,0 +1,4 @@ +export * from './types/index.js' +export * from './constants' +export * from './errors' +export * from './utils' diff --git a/shared/modules/ai/index.ts b/shared/modules/ai/index.ts new file mode 100644 index 0000000..60ced50 --- /dev/null +++ b/shared/modules/ai/index.ts @@ -0,0 +1,15 @@ +import { defineApiModule } from '../types.js' + +export const AI_MODULE = defineApiModule({ + id: 'ai', + name: 'AI', + basePath: '/ai', + order: 70, + version: '1.0.0', + frontend: { + enabled: false, + }, + backend: { + enabled: true, + }, +}) diff --git a/shared/modules/document-parser/index.ts b/shared/modules/document-parser/index.ts new file mode 100644 index 0000000..b94aacc --- /dev/null +++ b/shared/modules/document-parser/index.ts @@ -0,0 +1,15 @@ +import { defineApiModule } from '../types.js' + +export const DOCUMENT_PARSER_MODULE = defineApiModule({ + id: 'document-parser', + name: 'Document Parser', + basePath: '/document-parser', + order: 60, + version: '1.0.0', + frontend: { + enabled: false, + }, + backend: { + enabled: true, + }, +}) diff --git a/shared/modules/home/index.ts b/shared/modules/home/index.ts new file mode 100644 index 0000000..22a2c0e --- /dev/null +++ b/shared/modules/home/index.ts @@ -0,0 +1,12 @@ +import { defineModule } from '../types.js' + +export const HOME_MODULE = defineModule({ + id: 'home', + name: '首页', + basePath: '/home', + order: 0, + version: '1.0.0', + backend: { + enabled: false, + }, +}) diff --git a/shared/modules/index.ts b/shared/modules/index.ts new file mode 100644 index 0000000..fdc6332 --- /dev/null +++ b/shared/modules/index.ts @@ -0,0 +1 @@ +export * from './types.js' diff --git a/shared/modules/pydemos/api.ts b/shared/modules/pydemos/api.ts new file mode 100644 index 0000000..f529321 --- /dev/null +++ b/shared/modules/pydemos/api.ts @@ -0,0 +1,10 @@ +import { defineEndpoints } from '../types.js' + +export const PYDEMOS_ENDPOINTS = defineEndpoints({ + list: { path: '/', method: 'GET' }, + create: { path: '/create', method: 'POST' }, + delete: { path: '/delete', method: 'DELETE' }, + rename: { path: '/rename', method: 'POST' }, +}) + +export type PyDemosEndpoints = typeof PYDEMOS_ENDPOINTS diff --git a/shared/modules/pydemos/index.ts b/shared/modules/pydemos/index.ts new file mode 100644 index 0000000..840c18d --- /dev/null +++ b/shared/modules/pydemos/index.ts @@ -0,0 +1,15 @@ +import { defineApiModule } from '../types.js' +import { PYDEMOS_ENDPOINTS } from './api.js' + +export * from './api.js' + +export const PYDEMOS_MODULE = defineApiModule({ + id: 'pydemos', + name: 'Python Demos', + basePath: '/pydemos', + order: 50, + version: '1.0.0', + endpoints: PYDEMOS_ENDPOINTS, +}) + +export type { PyDemosEndpoints } from './api.js' diff --git a/shared/modules/recycle-bin/api.ts b/shared/modules/recycle-bin/api.ts new file mode 100644 index 0000000..9d9f401 --- /dev/null +++ b/shared/modules/recycle-bin/api.ts @@ -0,0 +1,10 @@ +import { defineEndpoints } from '../types.js' + +export const RECYCLE_BIN_ENDPOINTS = defineEndpoints({ + list: { path: '/', method: 'GET' }, + restore: { path: '/restore', method: 'POST' }, + permanent: { path: '/permanent', method: 'DELETE' }, + empty: { path: '/empty', method: 'DELETE' }, +}) + +export type RecycleBinEndpoints = typeof RECYCLE_BIN_ENDPOINTS diff --git a/shared/modules/recycle-bin/index.ts b/shared/modules/recycle-bin/index.ts new file mode 100644 index 0000000..4b40439 --- /dev/null +++ b/shared/modules/recycle-bin/index.ts @@ -0,0 +1,15 @@ +import { defineApiModule } from '../types.js' +import { RECYCLE_BIN_ENDPOINTS } from './api.js' + +export * from './api.js' + +export const RECYCLE_BIN_MODULE = defineApiModule({ + id: 'recycle-bin', + name: '回收站', + basePath: '/recycle-bin', + order: 40, + version: '1.0.0', + endpoints: RECYCLE_BIN_ENDPOINTS, +}) + +export type { RecycleBinEndpoints } from './api.js' diff --git a/shared/modules/remote/api.ts b/shared/modules/remote/api.ts new file mode 100644 index 0000000..5ad2186 --- /dev/null +++ b/shared/modules/remote/api.ts @@ -0,0 +1,12 @@ +import { defineEndpoints } from '../types.js' + +export const REMOTE_ENDPOINTS = defineEndpoints({ + getConfig: { path: '/config', method: 'GET' }, + saveConfig: { path: '/config', method: 'POST' }, + getScreenshot: { path: '/screenshot', method: 'GET' }, + saveScreenshot: { path: '/screenshot', method: 'POST' }, + getData: { path: '/data', method: 'GET' }, + saveData: { path: '/data', method: 'POST' }, +}) + +export type RemoteEndpoints = typeof REMOTE_ENDPOINTS diff --git a/shared/modules/remote/index.ts b/shared/modules/remote/index.ts new file mode 100644 index 0000000..a7aec38 --- /dev/null +++ b/shared/modules/remote/index.ts @@ -0,0 +1,16 @@ +import { defineApiModule } from '../types.js' +import { REMOTE_ENDPOINTS } from './api.js' + +export * from './types.js' +export * from './api.js' + +export const REMOTE_MODULE = defineApiModule({ + id: 'remote', + name: '远程', + basePath: '/remote', + order: 25, + version: '1.0.0', + endpoints: REMOTE_ENDPOINTS, +}) + +export type { RemoteEndpoints } from './api.js' diff --git a/shared/modules/remote/types.ts b/shared/modules/remote/types.ts new file mode 100644 index 0000000..b871d44 --- /dev/null +++ b/shared/modules/remote/types.ts @@ -0,0 +1,11 @@ +export interface RemoteDevice { + id: string + deviceName: string + serverHost: string + desktopPort: number + gitPort: number +} + +export interface RemoteConfig { + devices: RemoteDevice[] +} diff --git a/shared/modules/search/index.ts b/shared/modules/search/index.ts new file mode 100644 index 0000000..b25b590 --- /dev/null +++ b/shared/modules/search/index.ts @@ -0,0 +1,12 @@ +import { defineModule } from '../types.js' + +export const SEARCH_MODULE = defineModule({ + id: 'search', + name: '搜索', + basePath: '/search', + order: 10, + version: '1.0.0', + backend: { + enabled: false, + }, +}) diff --git a/shared/modules/settings/index.ts b/shared/modules/settings/index.ts new file mode 100644 index 0000000..5588a51 --- /dev/null +++ b/shared/modules/settings/index.ts @@ -0,0 +1,12 @@ +import { defineModule } from '../types.js' + +export const SETTINGS_MODULE = defineModule({ + id: 'settings', + name: '设置', + basePath: '/settings', + order: 100, + version: '1.0.0', + backend: { + enabled: false, + }, +}) diff --git a/shared/modules/time-tracking/api.ts b/shared/modules/time-tracking/api.ts new file mode 100644 index 0000000..648013e --- /dev/null +++ b/shared/modules/time-tracking/api.ts @@ -0,0 +1,13 @@ +import { defineEndpoints } from '../types.js' + +export const TIME_TRACKING_ENDPOINTS = defineEndpoints({ + current: { path: '/current', method: 'GET' }, + event: { path: '/event', method: 'POST' }, + day: { path: '/day/:date', method: 'GET' }, + week: { path: '/week/:startDate', method: 'GET' }, + month: { path: '/month/:yearMonth', method: 'GET' }, + year: { path: '/year/:year', method: 'GET' }, + stats: { path: '/stats', method: 'GET' }, +}) + +export type TimeTrackingEndpoints = typeof TIME_TRACKING_ENDPOINTS diff --git a/shared/modules/time-tracking/index.ts b/shared/modules/time-tracking/index.ts new file mode 100644 index 0000000..f3c4030 --- /dev/null +++ b/shared/modules/time-tracking/index.ts @@ -0,0 +1,15 @@ +import { defineApiModule } from '../types.js' +import { TIME_TRACKING_ENDPOINTS } from './api.js' + +export * from './api.js' + +export const TIME_TRACKING_MODULE = defineApiModule({ + id: 'time-tracking', + name: '时间统计', + basePath: '/time', + order: 20, + version: '1.0.0', + endpoints: TIME_TRACKING_ENDPOINTS, +}) + +export type { TimeTrackingEndpoints } from './api.js' diff --git a/shared/modules/todo/api.ts b/shared/modules/todo/api.ts new file mode 100644 index 0000000..3e19dcd --- /dev/null +++ b/shared/modules/todo/api.ts @@ -0,0 +1,12 @@ +import { defineEndpoints } from '../types.js' + +export const TODO_ENDPOINTS = defineEndpoints({ + list: { path: '/', method: 'GET' }, + save: { path: '/save', method: 'POST' }, + add: { path: '/add', method: 'POST' }, + toggle: { path: '/toggle', method: 'POST' }, + update: { path: '/update', method: 'POST' }, + delete: { path: '/delete', method: 'DELETE' }, +}) + +export type TodoEndpoints = typeof TODO_ENDPOINTS diff --git a/shared/modules/todo/index.ts b/shared/modules/todo/index.ts new file mode 100644 index 0000000..36855a9 --- /dev/null +++ b/shared/modules/todo/index.ts @@ -0,0 +1,16 @@ +import { defineApiModule } from '../types.js' +import { TODO_ENDPOINTS } from './api.js' + +export * from './types.js' +export * from './api.js' + +export const TODO_MODULE = defineApiModule({ + id: 'todo', + name: 'TODO', + basePath: '/todo', + order: 30, + version: '1.0.0', + endpoints: TODO_ENDPOINTS, +}) + +export type { TodoEndpoints } from './api.js' diff --git a/shared/modules/todo/types.ts b/shared/modules/todo/types.ts new file mode 100644 index 0000000..e226305 --- /dev/null +++ b/shared/modules/todo/types.ts @@ -0,0 +1,22 @@ +import type { DayTodo } from '../../types/todo.js' + +export interface TodoFilePath { + relPath: string + fullPath: string +} + +export interface ParsedTodoFile { + fullPath: string + dayTodos: DayTodo[] +} + +export interface GetTodoResult { + dayTodos: DayTodo[] + year: number + month: number +} + +export interface MigrationContext { + todayStr: string + yesterdayStr: string +} diff --git a/shared/modules/types.ts b/shared/modules/types.ts new file mode 100644 index 0000000..2290c77 --- /dev/null +++ b/shared/modules/types.ts @@ -0,0 +1,83 @@ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + +export interface EndpointDefinition { + path: string + method: HttpMethod + description?: string +} + +export interface ModuleEndpoints { + [key: string]: EndpointDefinition +} + +export interface ModuleFrontendConfig { + enabled?: boolean + icon?: string + component?: string +} + +export interface ModuleBackendConfig { + enabled?: boolean + createModule?: string +} + +export interface ModuleDefinition< + TId extends string = string, + TEndpoints extends ModuleEndpoints = ModuleEndpoints +> { + id: TId + name: string + basePath: string + order: number + version?: string + endpoints?: TEndpoints + dependencies?: string[] + icon?: string + frontend?: ModuleFrontendConfig + backend?: ModuleBackendConfig +} + +export interface ApiModuleConfig< + TId extends string = string, + TEndpoints extends ModuleEndpoints = ModuleEndpoints +> extends ModuleDefinition { + version: string +} + +export function defineModule< + TId extends string, + TEndpoints extends ModuleEndpoints +>( + config: ModuleDefinition +): ModuleDefinition { + return config +} + +export function defineApiModule< + TId extends string, + TEndpoints extends ModuleEndpoints +>( + config: ApiModuleConfig +): ApiModuleConfig { + return config +} + +export function defineEndpoints( + endpoints: TEndpoints +): TEndpoints { + return endpoints +} + +export type ExtractEndpointPaths = { + [K in keyof TEndpoints]: TEndpoints[K]['path'] +} + +export type ExtractEndpointMethods = { + [K in keyof TEndpoints]: TEndpoints[K]['method'] +} + +export type EndpointConfig = EndpointDefinition + +export interface ModuleApiConfig { + endpoints: ModuleEndpoints +} diff --git a/shared/modules/weread/index.ts b/shared/modules/weread/index.ts new file mode 100644 index 0000000..bcd44df --- /dev/null +++ b/shared/modules/weread/index.ts @@ -0,0 +1,12 @@ +import { defineModule } from '../types.js' + +export const WEREAD_MODULE = defineModule({ + id: 'weread', + name: '微信读书', + basePath: '/weread', + order: 20, + version: '1.0.0', + backend: { + enabled: false, + }, +}) diff --git a/shared/types.ts b/shared/types.ts new file mode 100644 index 0000000..a62b765 --- /dev/null +++ b/shared/types.ts @@ -0,0 +1 @@ +export * from './types/index.js' diff --git a/shared/types/api.ts b/shared/types/api.ts new file mode 100644 index 0000000..d97b48e --- /dev/null +++ b/shared/types/api.ts @@ -0,0 +1,9 @@ +export type ApiErrorDTO = { + code: string + message: string + details?: unknown +} + +export type ApiResponse = + | { success: true; data: T } + | { success: false; error: ApiErrorDTO } diff --git a/shared/types/file.ts b/shared/types/file.ts new file mode 100644 index 0000000..681dd63 --- /dev/null +++ b/shared/types/file.ts @@ -0,0 +1,22 @@ +export type FileKind = 'file' | 'dir' + +export interface FileItemDTO { + name: string + type: FileKind + size: number + modified: string + path: string +} + +export interface FileContentDTO { + content: string + metadata: { + size: number + modified: string + } +} + +export type PathExistsDTO = { + exists: boolean + type: FileKind | null +} diff --git a/shared/types/index.ts b/shared/types/index.ts new file mode 100644 index 0000000..630fca9 --- /dev/null +++ b/shared/types/index.ts @@ -0,0 +1,9 @@ +export * from './file.js' +export * from './module.js' +export * from './tab.js' +export * from './time.js' +export * from './todo.js' +export * from './pydemos.js' +export * from './recycle-bin.js' +export * from './settings.js' +export * from './api.js' diff --git a/shared/types/module.ts b/shared/types/module.ts new file mode 100644 index 0000000..8683a2c --- /dev/null +++ b/shared/types/module.ts @@ -0,0 +1,34 @@ +import type { LucideIcon } from 'lucide-react' +import type { FileItemDTO as FileItem } from './file.js' + +export type { + HttpMethod, + EndpointConfig, + EndpointDefinition, + ModuleEndpoints, + ModuleApiConfig, + ModuleDefinition, + ApiModuleConfig, +} from '../modules/types.js' + +export type Brand = T & { __brand: TBrand } +export type ModuleId = Brand + +import type { ModuleDefinition, ModuleEndpoints } from '../modules/types.js' + +export interface FrontendModuleConfig< + TEndpoints extends ModuleEndpoints = ModuleEndpoints +> extends Omit, 'icon' | 'basePath'> { + basePath?: string + icon: LucideIcon + component: React.ComponentType +} + +export interface InternalModuleConfig extends FrontendModuleConfig { + tabId: string + fileItem: FileItem +} + +export const createModuleId = (id: string): ModuleId => { + return id as ModuleId +} diff --git a/shared/types/pydemos.ts b/shared/types/pydemos.ts new file mode 100644 index 0000000..a5be031 --- /dev/null +++ b/shared/types/pydemos.ts @@ -0,0 +1,11 @@ +export interface PyDemoItem { + name: string + path: string + created: string + fileCount: number +} + +export interface PyDemoMonth { + month: number + demos: PyDemoItem[] +} diff --git a/shared/types/recycle-bin.ts b/shared/types/recycle-bin.ts new file mode 100644 index 0000000..cd04455 --- /dev/null +++ b/shared/types/recycle-bin.ts @@ -0,0 +1,14 @@ +import type { FileKind } from './file.js' + +export interface RecycleBinItemDTO { + name: string + originalName: string + type: FileKind + deletedDate: string + path: string +} + +export interface RecycleBinGroupDTO { + date: string + items: RecycleBinItemDTO[] +} diff --git a/shared/types/settings.ts b/shared/types/settings.ts new file mode 100644 index 0000000..bc70866 --- /dev/null +++ b/shared/types/settings.ts @@ -0,0 +1,7 @@ +export type ThemeMode = 'light' | 'dark' + +export interface SettingsDTO { + theme?: 'light' | 'dark' + wallpaperOpacity?: number + markdownFontSize?: number +} diff --git a/shared/types/tab.ts b/shared/types/tab.ts new file mode 100644 index 0000000..5c10021 --- /dev/null +++ b/shared/types/tab.ts @@ -0,0 +1 @@ +export type TabType = 'markdown' | 'todo' | 'settings' | 'search' | 'recycle-bin' | 'weread' | 'time-tracking' | 'pydemos' | 'remote' | 'remote-desktop' | 'remote-git' | 'other' diff --git a/shared/types/time.ts b/shared/types/time.ts new file mode 100644 index 0000000..f64077a --- /dev/null +++ b/shared/types/time.ts @@ -0,0 +1,101 @@ +export type { TabType } from './tab.js' + +export interface TimePeriod { + start: string + end: string +} + +export interface TabRecord { + tabId: string + filePath: string | null + fileName: string + tabType: import('./tab.js').TabType + duration: number + focusedPeriods: TimePeriod[] +} + +export interface TimingSession { + id: string + startTime: string + endTime?: string + duration: number + status: 'active' | 'paused' | 'ended' + tabRecords: TabRecord[] +} + +export interface TabSummary { + fileName: string + tabType: import('./tab.js').TabType + totalDuration: number + focusCount: number +} + +export interface DayTimeData { + date: string + totalDuration: number + sessions: TimingSession[] + tabSummary: Record + lastUpdated: string +} + +export interface DaySummary { + totalDuration: number + sessions: number + topTabs: Array<{ fileName: string; duration: number }> +} + +export interface MonthTimeData { + year: number + month: number + days: Record + monthlyTotal: number + averageDaily: number + activeDays: number + lastUpdated: string +} + +export interface YearTimeData { + year: number + months: Record + yearlyTotal: number + averageMonthly: number + averageDaily: number + totalActiveDays: number +} + +export interface CurrentTimerState { + isRunning: boolean + isPaused: boolean + currentSession: { + id: string + startTime: string + duration: number + currentTab: { + tabId: string + fileName: string + tabType: import('./tab.js').TabType + } | null + } | null + todayDuration: number +} + +export interface TimeStats { + totalDuration: number + activeDays: number + averageDaily: number + longestDay: { date: string; duration: number } | null + longestSession: { date: string; duration: number } | null + topTabs: Array<{ fileName: string; duration: number; percentage: number }> + tabTypeDistribution: Array<{ tabType: import('./tab.js').TabType; duration: number; percentage: number }> +} + +export interface TimeTrackingEvent { + type: 'tab-switch' | 'tab-open' | 'tab-close' | 'window-focus' | 'window-blur' | 'app-quit' | 'heartbeat' + timestamp: string + tabInfo?: { + tabId: string + filePath: string | null + fileName: string + tabType: import('./tab.js').TabType + } +} diff --git a/shared/types/todo.ts b/shared/types/todo.ts new file mode 100644 index 0000000..dfdecfb --- /dev/null +++ b/shared/types/todo.ts @@ -0,0 +1,10 @@ +export interface TodoItem { + id: string + content: string + completed: boolean +} + +export interface DayTodo { + date: string + items: TodoItem[] +} diff --git a/shared/utils/date.ts b/shared/utils/date.ts new file mode 100644 index 0000000..4052c92 --- /dev/null +++ b/shared/utils/date.ts @@ -0,0 +1,159 @@ +export const pad2 = (n: number) => String(n).padStart(2, '0') + +export const pad3 = (n: number) => String(n).padStart(3, '0') + +export const formatTimestamp = (d: Date) => { + const yyyy = d.getFullYear() + const mm = pad2(d.getMonth() + 1) + const dd = pad2(d.getDate()) + const hh = pad2(d.getHours()) + const mi = pad2(d.getMinutes()) + const ss = pad2(d.getSeconds()) + const ms = pad3(d.getMilliseconds()) + return `${yyyy}${mm}${dd}_${hh}${mi}${ss}_${ms}` +} + +export const formatDate = (date: Date) => { + const yyyy = date.getFullYear() + const mm = pad2(date.getMonth() + 1) + const dd = pad2(date.getDate()) + return `${yyyy}-${mm}-${dd}` +} + +export const formatTime = (date: Date) => { + const hh = pad2(date.getHours()) + const mm = pad2(date.getMinutes()) + const ss = pad2(date.getSeconds()) + return `${hh}:${mm}:${ss}` +} + +export const formatDateTime = (date: Date) => { + return `${formatDate(date)} ${formatTime(date)}` +} + +export const getTodayDate = (): string => { + return formatDate(new Date()) +} + +export const getTomorrowDate = (): string => { + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + return formatDate(tomorrow) +} + +export const formatDateDisplay = (dateStr: string): string => { + const today = getTodayDate() + const tomorrow = getTomorrowDate() + if (dateStr === today) return '今天' + if (dateStr === tomorrow) return '明天' + const [year, month, day] = dateStr.split('-') + const currentYear = new Date().getFullYear() + if (parseInt(year) === currentYear) { + return `${parseInt(month)}月${parseInt(day)}日` + } + return `${year}年${parseInt(month)}月${parseInt(day)}日` +} + +export const formatDuration = (ms: number): string => { + const seconds = Math.floor(ms / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + if (hours > 0) { + const remainingMinutes = minutes % 60 + return `${hours}小时${remainingMinutes > 0 ? `${remainingMinutes}分钟` : ''}` + } + if (minutes > 0) { + return `${minutes}分钟` + } + return `${seconds}秒` +} + +export const formatDurationShort = (ms: number): string => { + const seconds = Math.floor(ms / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + if (hours > 0) { + const remainingMinutes = minutes % 60 + return remainingMinutes > 0 ? `${hours}h${remainingMinutes}m` : `${hours}h` + } + if (minutes > 0) { + return `${minutes}m` + } + return `${seconds}s` +} + +export const getWeekStart = (date: Date): Date => { + const d = new Date(date) + const day = d.getDay() + const diff = d.getDate() - day + (day === 0 ? -6 : 1) + d.setDate(diff) + d.setHours(0, 0, 0, 0) + return d +} + +export const getWeekEnd = (date: Date): Date => { + const weekStart = getWeekStart(date) + const weekEnd = new Date(weekStart) + weekEnd.setDate(weekStart.getDate() + 6) + return weekEnd +} + +export const getMonthStart = (date: Date): Date => { + return new Date(date.getFullYear(), date.getMonth(), 1) +} + +export const getMonthEnd = (date: Date): Date => { + return new Date(date.getFullYear(), date.getMonth() + 1, 0) +} + +export const getYearStart = (date: Date): Date => { + return new Date(date.getFullYear(), 0, 1) +} + +export const getYearEnd = (date: Date): Date => { + return new Date(date.getFullYear(), 11, 31) +} + +export const isSameDay = (date1: Date, date2: Date): boolean => { + return formatDate(date1) === formatDate(date2) +} + +export const isToday = (date: Date): boolean => { + return isSameDay(date, new Date()) +} + +export const addDays = (date: Date, days: number): Date => { + const result = new Date(date) + result.setDate(result.getDate() + days) + return result +} + +export const addMonths = (date: Date, months: number): Date => { + const result = new Date(date) + result.setMonth(result.getMonth() + months) + return result +} + +export const addYears = (date: Date, years: number): Date => { + const result = new Date(date) + result.setFullYear(result.getFullYear() + years) + return result +} + +export const parseDate = (dateStr: string): Date | null => { + const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/) + if (!match) return null + const [, year, month, day] = match + return new Date(parseInt(year), parseInt(month) - 1, parseInt(day)) +} + +export const getDaysInMonth = (year: number, month: number): number => { + return new Date(year, month, 0).getDate() +} + +export const getDayOfWeek = (date: Date): string => { + const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + return days[date.getDay()] +} diff --git a/shared/utils/index.ts b/shared/utils/index.ts new file mode 100644 index 0000000..a141ba3 --- /dev/null +++ b/shared/utils/index.ts @@ -0,0 +1,3 @@ +export * from './path' +export * from './tabType' +export * from './date' diff --git a/shared/utils/path.ts b/shared/utils/path.ts new file mode 100644 index 0000000..a6398c1 --- /dev/null +++ b/shared/utils/path.ts @@ -0,0 +1,54 @@ +export const toPosixPath = (p: string) => p.replace(/\\/g, '/') + +export const toWindowsPath = (p: string) => p.replace(/\//g, '\\') + +export const normalizePath = (p: string) => { + const parts = p.replace(/\\/g, '/').split('/') + const stack: string[] = [] + for (const part of parts) { + if (!part || part === '.') continue + if (part === '..') { + stack.pop() + continue + } + stack.push(part) + } + return stack.join('/') +} + +export const joinPaths = (...paths: string[]) => { + return paths.map(normalizePath).join('/').replace(/\/+/g, '/') +} + +export const getDirectoryName = (filePath: string) => { + const normalized = normalizePath(filePath) + const lastSlash = normalized.lastIndexOf('/') + return lastSlash === -1 ? '' : normalized.slice(0, lastSlash) +} + +export const getFileName = (filePath: string) => { + const normalized = normalizePath(filePath) + const lastSlash = normalized.lastIndexOf('/') + return lastSlash === -1 ? normalized : normalized.slice(lastSlash + 1) +} + +export const getFileExtension = (filePath: string) => { + const fileName = getFileName(filePath) + const lastDot = fileName.lastIndexOf('.') + return lastDot === -1 ? '' : fileName.slice(lastDot + 1) +} + +export const removeExtension = (filePath: string) => { + const ext = getFileExtension(filePath) + if (!ext) return filePath + return filePath.slice(0, -(ext.length + 1)) +} + +export const isAbsolutePath = (p: string) => { + return p.startsWith('/') || /^[A-Za-z]:/.test(p) +} + +export const isHiddenPath = (p: string) => { + const normalized = normalizePath(p) + return normalized.split('/').some(part => part.startsWith('.')) +} diff --git a/shared/utils/tabType.ts b/shared/utils/tabType.ts new file mode 100644 index 0000000..366b2a0 --- /dev/null +++ b/shared/utils/tabType.ts @@ -0,0 +1,65 @@ +import type { TabType } from '../types/tab.js' + +const KNOWN_MODULE_IDS = [ + 'home', 'settings', 'search', 'weread', + 'recycle-bin', 'todo', 'time-tracking', 'pydemos' +] as const + +export function getTabTypeFromPath(filePath: string | null): TabType { + if (!filePath) return 'other' + + if (filePath.startsWith('remote-git://')) { + return 'remote-git' + } + + if (filePath.startsWith('remote-desktop://')) { + return 'remote-desktop' + } + + if (filePath.startsWith('remote-') && filePath !== 'remote-tab') { + return 'remote-desktop' + } + + if (filePath === 'remote-tab' || filePath === 'remote') { + return 'remote' + } + + for (const moduleId of KNOWN_MODULE_IDS) { + if (filePath === `${moduleId}-tab` || filePath === moduleId) { + if (moduleId === 'home' || moduleId === 'settings' || moduleId === 'search' || moduleId === 'weread') { + return 'other' + } + return moduleId as TabType + } + } + + if (filePath.endsWith('.md')) { + return 'markdown' + } + + return 'other' +} + +export function getFileNameFromPath(filePath: string | null): string { + if (!filePath) return '未知' + + for (const moduleId of KNOWN_MODULE_IDS) { + if (filePath === `${moduleId}-tab` || filePath === moduleId) { + const names: Record = { + 'home': '首页', + 'settings': '设置', + 'search': '搜索', + 'weread': '微信读书', + 'recycle-bin': '回收站', + 'todo': 'TODO', + 'time-tracking': '时间统计', + 'pydemos': 'Python Demo', + 'remote': '远程桌面', + } + return names[moduleId] ?? moduleId + } + } + + const parts = filePath.split('/') + return parts[parts.length - 1] || filePath +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..fcff281 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,25 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { NoteBrowser } from '@/pages/NoteBrowser' +import { SettingsSync } from '@/components/settings/SettingsSync' +import { ErrorBoundary } from '@/components/common/ErrorBoundary' + +import '@/modules' + +import { TimeTrackerProvider } from '@/modules/time-tracking' + +function App() { + return ( + + + + + + } /> + + + + + ) +} + +export default App diff --git a/src/components/common/ConfirmDialog/ConfirmDialog.tsx b/src/components/common/ConfirmDialog/ConfirmDialog.tsx new file mode 100644 index 0000000..ba4b757 --- /dev/null +++ b/src/components/common/ConfirmDialog/ConfirmDialog.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { Modal } from '../Modal' +import { DialogContent } from '../DialogContent' + +interface ConfirmDialogProps { + isOpen: boolean + title: string + message: string + onConfirm: () => void + onCancel: () => void +} + +export const ConfirmDialog: React.FC = ({ + isOpen, + title, + message, + onConfirm, + onCancel, +}) => { + return ( + + +

{message}

+
+
+ ) +} diff --git a/src/components/common/ConfirmDialog/index.ts b/src/components/common/ConfirmDialog/index.ts new file mode 100644 index 0000000..114def4 --- /dev/null +++ b/src/components/common/ConfirmDialog/index.ts @@ -0,0 +1 @@ +export { ConfirmDialog } from './ConfirmDialog' diff --git a/src/components/common/ContextMenu/ContextMenu.tsx b/src/components/common/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000..c1752b5 --- /dev/null +++ b/src/components/common/ContextMenu/ContextMenu.tsx @@ -0,0 +1,50 @@ +import React, { useRef } from 'react' +import { createPortal } from 'react-dom' +import { useClickOutside } from '@/hooks/utils/useClickOutside' + +export interface ContextMenuItem { + label: string + onClick: () => void +} + +export interface ContextMenuProps { + items: ContextMenuItem[] + isOpen: boolean + position: { x: number; y: number } + onClose: () => void +} + +export const ContextMenu: React.FC = ({ items, isOpen, position, onClose }) => { + const menuRef = useRef(null) + + useClickOutside(menuRef, onClose, { + enabled: isOpen, + includeEscape: true + }) + + if (!isOpen || items.length === 0) { + return null + } + + return createPortal( +
+ {items.map((item, index) => ( +
+ {item.label} +
+ ))} +
, + document.body + ) +} diff --git a/src/components/common/ContextMenu/index.ts b/src/components/common/ContextMenu/index.ts new file mode 100644 index 0000000..87628ed --- /dev/null +++ b/src/components/common/ContextMenu/index.ts @@ -0,0 +1,2 @@ +export { ContextMenu } from './ContextMenu' +export type { ContextMenuItem, ContextMenuProps } from './ContextMenu' diff --git a/src/components/common/DialogContent/DialogContent.tsx b/src/components/common/DialogContent/DialogContent.tsx new file mode 100644 index 0000000..8d1cf00 --- /dev/null +++ b/src/components/common/DialogContent/DialogContent.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import { Loader2 } from 'lucide-react' + +interface DialogContentProps { + title: string + children: React.ReactNode + + footer?: React.ReactNode + onConfirm?: () => void + onCancel?: () => void + confirmText?: string + cancelText?: string + + isConfirmDisabled?: boolean + isConfirmLoading?: boolean + + confirmButtonVariant?: 'primary' | 'danger' + confirmButtonType?: 'button' | 'submit' +} + +export const DialogContent = ({ + title, + children, + footer, + onConfirm, + onCancel, + confirmText = '确认', + cancelText = '取消', + isConfirmDisabled = false, + isConfirmLoading = false, + confirmButtonVariant = 'primary', + confirmButtonType = 'button' +}: DialogContentProps) => { + const confirmButtonClass = confirmButtonVariant === 'danger' + ? "px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" + : "px-4 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" + + const showConfirmButton = onConfirm || confirmButtonType === 'submit' + + return ( +
+

+ {title} +

+ +
+ {children} +
+ + {footer ? ( + footer + ) : ( +
+ {onCancel && ( + + )} + {showConfirmButton && ( + + )} +
+ )} +
+ ) +} diff --git a/src/components/common/DialogContent/index.ts b/src/components/common/DialogContent/index.ts new file mode 100644 index 0000000..9f2bbae --- /dev/null +++ b/src/components/common/DialogContent/index.ts @@ -0,0 +1 @@ +export { DialogContent } from './DialogContent' diff --git a/src/components/common/ErrorBoundary/ErrorBoundary.tsx b/src/components/common/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000..43bcf23 --- /dev/null +++ b/src/components/common/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,93 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react' +import { AlertTriangle, RefreshCw } from 'lucide-react' + +interface Props { + children: ReactNode + fallback?: ReactNode +} + +interface State { + hasError: boolean + error: Error | null + errorInfo: ErrorInfo | null +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { + hasError: false, + error: null, + errorInfo: null + } + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ errorInfo }) + console.error('ErrorBoundary caught an error:', error, errorInfo) + } + + handleRetry = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null + }) + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback + } + + return ( +
+ +

+ 出现了一些问题 +

+

+ {this.state.error?.message || '发生了未知错误'} +

+ + {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( +
+ + 查看错误详情 + +
+                {this.state.error?.stack}
+              
+
+ )} +
+ ) + } + + return this.props.children + } +} + +export const withErrorBoundary =

( + WrappedComponent: React.ComponentType

, + fallback?: ReactNode +) => { + return function WithErrorBoundaryWrapper(props: P) { + return ( + + + + ) + } +} diff --git a/src/components/common/ErrorBoundary/index.ts b/src/components/common/ErrorBoundary/index.ts new file mode 100644 index 0000000..97d788f --- /dev/null +++ b/src/components/common/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { ErrorBoundary } from './ErrorBoundary' diff --git a/src/components/common/Modal/Modal.tsx b/src/components/common/Modal/Modal.tsx new file mode 100644 index 0000000..d93cf3e --- /dev/null +++ b/src/components/common/Modal/Modal.tsx @@ -0,0 +1,56 @@ +import React, { useEffect } from 'react' +import { createPortal } from 'react-dom' + +interface ModalProps { + isOpen: boolean + onClose: () => void + children: React.ReactNode + className?: string + closeOnOverlayClick?: boolean +} + +export const Modal: React.FC = ({ + isOpen, + onClose, + children, + className = 'max-w-md', + closeOnOverlayClick = false +}) => { + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose() + } + } + + if (isOpen) { + document.addEventListener('keydown', handleEscape) + document.body.style.overflow = 'hidden' + } + + return () => { + document.removeEventListener('keydown', handleEscape) + document.body.style.overflow = 'unset' + } + }, [isOpen, onClose]) + + if (!isOpen) return null + if (typeof document === 'undefined') return null + + return createPortal( +

+
e.stopPropagation()} + > + {children} +
+
, + document.body + ) +} diff --git a/src/components/common/Modal/index.ts b/src/components/common/Modal/index.ts new file mode 100644 index 0000000..41e7f5e --- /dev/null +++ b/src/components/common/Modal/index.ts @@ -0,0 +1 @@ +export { Modal } from './Modal' diff --git a/src/components/common/Select/Select.tsx b/src/components/common/Select/Select.tsx new file mode 100644 index 0000000..58dcb5e --- /dev/null +++ b/src/components/common/Select/Select.tsx @@ -0,0 +1,96 @@ +import React, { useState, useRef } from 'react' +import { ChevronDown, Check } from 'lucide-react' +import { useClickOutside } from '@/hooks/utils/useClickOutside' + +export interface SelectOption { + value: string + label: string + icon?: React.ReactNode +} + +interface SelectProps { + label?: string + value: string + options: SelectOption[] + onChange: (value: string) => void + className?: string +} + +export const Select: React.FC = ({ + label, + value, + options, + onChange, + className = '', +}) => { + const [isOpen, setIsOpen] = useState(false) + const containerRef = useRef(null) + + const selectedOption = options.find((opt) => opt.value === value) || options[0] + + useClickOutside(containerRef, () => setIsOpen(false), { + enabled: isOpen + }) + + return ( +
+ {label && ( + + )} + + + {isOpen && ( +
+
    + {options.map((option) => ( +
  • { + onChange(option.value) + setIsOpen(false) + }} + className={`px-3 py-2 cursor-pointer flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors ${ + value === option.value + ? 'bg-gray-100 dark:bg-gray-600 text-gray-900 dark:text-gray-100' + : 'text-gray-700 dark:text-gray-200' + }`} + > +
    + {option.icon && ( + + {option.icon} + + )} + {option.label} +
    + {value === option.value && } +
  • + ))} +
+
+ )} +
+ ) +} diff --git a/src/components/common/Select/index.ts b/src/components/common/Select/index.ts new file mode 100644 index 0000000..948b2ea --- /dev/null +++ b/src/components/common/Select/index.ts @@ -0,0 +1,2 @@ +export { Select } from './Select' +export type { SelectOption } from './Select' diff --git a/src/components/common/__tests__/ConfirmDialog.test.tsx b/src/components/common/__tests__/ConfirmDialog.test.tsx new file mode 100644 index 0000000..b239457 --- /dev/null +++ b/src/components/common/__tests__/ConfirmDialog.test.tsx @@ -0,0 +1,91 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, cleanup } from '@testing-library/react' +import { ConfirmDialog } from '../ConfirmDialog' + +describe('ConfirmDialog', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + cleanup() + vi.useRealTimers() + }) + + it('显示标题和消息', () => { + const mockOnConfirm = vi.fn() + const mockOnCancel = vi.fn() + + render( + + ) + + expect(screen.getByText('确认删除')).toBeInTheDocument() + expect(screen.getByText('确定要删除这个项目吗?')).toBeInTheDocument() + }) + + it('点击确认按钮触发 onConfirm', () => { + const mockOnConfirm = vi.fn() + const mockOnCancel = vi.fn() + + render( + + ) + + const confirmButton = screen.getByRole('button', { name: '确定' }) + fireEvent.click(confirmButton) + + expect(mockOnConfirm).toHaveBeenCalledTimes(1) + expect(mockOnCancel).not.toHaveBeenCalled() + }) + + it('点击取消按钮触发 onCancel', () => { + const mockOnConfirm = vi.fn() + const mockOnCancel = vi.fn() + + render( + + ) + + const cancelButton = screen.getByRole('button', { name: '取消' }) + fireEvent.click(cancelButton) + + expect(mockOnCancel).toHaveBeenCalledTimes(1) + expect(mockOnConfirm).not.toHaveBeenCalled() + }) + + it('关闭时不渲染内容', () => { + const mockOnConfirm = vi.fn() + const mockOnCancel = vi.fn() + + render( + + ) + + expect(screen.queryByText('确认删除')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/common/__tests__/ErrorBoundary.test.tsx b/src/components/common/__tests__/ErrorBoundary.test.tsx new file mode 100644 index 0000000..cbd8009 --- /dev/null +++ b/src/components/common/__tests__/ErrorBoundary.test.tsx @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, cleanup } from '@testing-library/react' +import { ErrorBoundary } from '../ErrorBoundary' +import React from 'react' + +describe('ErrorBoundary', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + cleanup() + vi.useRealTimers() + }) + + it('正常渲染子组件', () => { + render( + +
Child Content
+
+ ) + + expect(screen.getByText('Child Content')).toBeInTheDocument() + }) + + it('捕获子组件错误并显示错误信息', () => { + const ThrowError: React.FC = () => { + throw new Error('Test error') + } + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + render( + + + + ) + + vi.runAllTimers() + + expect(screen.getByText('出现了一些问题')).toBeInTheDocument() + expect(screen.getByText('Test error')).toBeInTheDocument() + + consoleErrorSpy.mockRestore() + }) + + it('错误边界不会捕获自身错误', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + render( + +
Normal Content
+
+ ) + + expect(screen.getByText('Normal Content')).toBeInTheDocument() + + consoleErrorSpy.mockRestore() + }) + + it('点击重试按钮恢复', () => { + let shouldThrow = true + + const ThrowError: React.FC = () => { + if (shouldThrow) { + throw new Error('Test error') + } + return
Recovered Content
+ } + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const { rerender } = render( + + + + ) + + vi.runAllTimers() + + expect(screen.getByText('出现了一些问题')).toBeInTheDocument() + + shouldThrow = false + rerender( + + + + ) + + vi.runAllTimers() + + const retryButton = screen.getByRole('button', { name: /重试/i }) + fireEvent.click(retryButton) + + expect(screen.queryByText('出现了一些问题')).not.toBeInTheDocument() + + consoleErrorSpy.mockRestore() + }) + + it('使用自定义 fallback', () => { + const ThrowError: React.FC = () => { + throw new Error('Test error') + } + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + render( + Custom Fallback}> + + + ) + + vi.runAllTimers() + + expect(screen.getByText('Custom Fallback')).toBeInTheDocument() + expect(screen.queryByText('出现了一些问题')).not.toBeInTheDocument() + + consoleErrorSpy.mockRestore() + }) +}) diff --git a/src/components/common/__tests__/Modal.test.tsx b/src/components/common/__tests__/Modal.test.tsx new file mode 100644 index 0000000..041fc80 --- /dev/null +++ b/src/components/common/__tests__/Modal.test.tsx @@ -0,0 +1,85 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, cleanup } from '@testing-library/react' +import { Modal } from '../Modal' + +describe('Modal', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + cleanup() + vi.useRealTimers() + }) + + it('打开时渲染内容', () => { + const mockOnClose = vi.fn() + render( + +
Modal Content
+
+ ) + + expect(screen.getByText('Modal Content')).toBeInTheDocument() + }) + + it('关闭时不渲染内容', () => { + const mockOnClose = vi.fn() + render( + +
Modal Content
+
+ ) + + expect(screen.queryByText('Modal Content')).not.toBeInTheDocument() + }) + + it('点击遮罩层关闭', () => { + const mockOnClose = vi.fn() + render( + +
Modal Content
+
+ ) + + const overlay = screen.getByText('Modal Content').parentElement?.parentElement + if (overlay) { + fireEvent.click(overlay) + } + + expect(mockOnClose).toHaveBeenCalledTimes(1) + }) + + it('点击遮罩层不关闭 when closeOnOverlayClick is false', () => { + const mockOnClose = vi.fn() + render( + +
Modal Content
+
+ ) + + const overlay = screen.getByText('Modal Content').parentElement?.parentElement + if (overlay) { + fireEvent.click(overlay) + } + + expect(mockOnClose).not.toHaveBeenCalled() + }) + + it('显示关闭按钮并点击关闭', () => { + const mockOnClose = vi.fn() + render( + +
+

Modal Title

+ +
+
+ ) + + const closeButton = screen.getByRole('button', { name: '关闭' }) + fireEvent.click(closeButton) + + expect(mockOnClose).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/components/dialogs/CreateItemDialog/CreateItemDialog.tsx b/src/components/dialogs/CreateItemDialog/CreateItemDialog.tsx new file mode 100644 index 0000000..3b5c6ce --- /dev/null +++ b/src/components/dialogs/CreateItemDialog/CreateItemDialog.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Modal } from '@/components/common/Modal' +import { Select } from '@/components/common/Select' +import { File, FileText, Code } from 'lucide-react' +import { DialogContent } from '@/components/common/DialogContent' + +interface CreateItemDialogProps { + isOpen: boolean + title: string + placeholder: string + errorMessage?: string | null + initialValue?: string + onNameChange?: (name: string) => void + onSubmit: (name: string, initMethod: 'blank' | 'pdf' | 'html') => void + onCancel: () => void + showInitMethod?: boolean +} + +export const CreateItemDialog: React.FC = ({ + isOpen, + title, + placeholder, + errorMessage, + initialValue = '', + onNameChange, + onSubmit, + onCancel, + showInitMethod = false, +}) => { + const [name, setName] = useState('') + const [initMethod, setInitMethod] = useState<'blank' | 'pdf' | 'html'>('blank') + const inputRef = useRef(null) + + useEffect(() => { + if (isOpen) { + setName(initialValue) + setInitMethod('blank') + onNameChange?.(initialValue) + setTimeout(() => { + inputRef.current?.focus() + if (initialValue) { + inputRef.current?.select() + } + }, 100) + } + }, [isOpen, initialValue]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (initMethod === 'pdf' || initMethod === 'html') { + onSubmit('', initMethod) + return + } + if (name.trim()) { + onSubmit(name.trim(), initMethod) + } + } + + return ( + +
+ + {(!showInitMethod || initMethod === 'blank') && ( +
+ { + const next = e.target.value + setName(next) + onNameChange?.(next) + }} + autoComplete="off" + aria-invalid={Boolean(errorMessage)} + /> + {errorMessage ?
{errorMessage}
: null} +
+ )} + + {showInitMethod && ( +
+ { + setName(e.target.value) + setError(null) + }} + autoFocus + /> +
+ +
+ + + + +
fileInputRef.current?.click()} + > + +

+ 拖拽文件或文件夹到此处 +

+

+ 支持 .py, .md, .txt, .json, .yaml 等文件 +

+
+ +
+ +
+
+ + {selectedFiles.length > 0 && ( +
+
+ + +
+
+ {fileTree.map(item => ( + + ))} +
+
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ 将创建在 {year}年{month}月 +
+ +
+ + ) +} diff --git a/src/components/dialogs/CreatePyDemoDialog/index.ts b/src/components/dialogs/CreatePyDemoDialog/index.ts new file mode 100644 index 0000000..4c6d799 --- /dev/null +++ b/src/components/dialogs/CreatePyDemoDialog/index.ts @@ -0,0 +1 @@ +export { CreatePyDemoDialog } from './CreatePyDemoDialog' diff --git a/src/components/dialogs/DeleteConfirmDialog/DeleteConfirmDialog.tsx b/src/components/dialogs/DeleteConfirmDialog/DeleteConfirmDialog.tsx new file mode 100644 index 0000000..ad65935 --- /dev/null +++ b/src/components/dialogs/DeleteConfirmDialog/DeleteConfirmDialog.tsx @@ -0,0 +1,76 @@ +import React, { useRef, useState, useEffect, useCallback } from 'react' +import { Modal } from '@/components/common/Modal' +import { DialogContent } from '@/components/common/DialogContent' + +interface DeleteConfirmDialogProps { + isOpen: boolean + title: string + message: string + expectedText: string + confirmText?: string + buttonVariant?: 'primary' | 'danger' + onConfirm: () => void + onCancel: () => void +} + +export const DeleteConfirmDialog: React.FC = ({ + isOpen, + title, + message, + expectedText, + confirmText = '删除', + buttonVariant = 'danger', + onConfirm, + onCancel, +}) => { + const [inputValue, setInputValue] = useState('') + const inputRef = useRef(null) + + useEffect(() => { + if (!isOpen) return + setInputValue('') + const timeoutId = window.setTimeout(() => { + inputRef.current?.focus() + inputRef.current?.select() + }, 0) + return () => window.clearTimeout(timeoutId) + }, [isOpen]) + + const isMatch = inputValue === expectedText + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault() + if (!isMatch) return + onConfirm() + }, [isMatch, onConfirm]) + + return ( + + + +

{message}

+ +
+ + setInputValue(e.target.value)} + ref={inputRef} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-gray-500 focus:border-gray-500 sm:text-sm dark:bg-gray-700 dark:text-gray-100" + placeholder={expectedText} + /> +
+
+ +
+ ) +} diff --git a/src/components/dialogs/DeleteConfirmDialog/index.ts b/src/components/dialogs/DeleteConfirmDialog/index.ts new file mode 100644 index 0000000..da86175 --- /dev/null +++ b/src/components/dialogs/DeleteConfirmDialog/index.ts @@ -0,0 +1 @@ +export { DeleteConfirmDialog } from './DeleteConfirmDialog' diff --git a/src/components/dialogs/ExportDialog/ExportDialog.tsx b/src/components/dialogs/ExportDialog/ExportDialog.tsx new file mode 100644 index 0000000..3575563 --- /dev/null +++ b/src/components/dialogs/ExportDialog/ExportDialog.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { Modal } from '@/components/common/Modal' +import { FileText } from 'lucide-react' +import { DialogContent } from '@/components/common/DialogContent' + +interface ExportDialogProps { + isOpen: boolean + onClose: () => void + onExportMarkdown: () => void + onExportPDF: () => void + isExporting: boolean +} + +export const ExportDialog: React.FC = ({ + isOpen, + onClose, + onExportMarkdown, + onExportPDF, + isExporting +}) => { + return ( + + +
+ + + +
+
+
+ ) +} diff --git a/src/components/dialogs/ExportDialog/index.ts b/src/components/dialogs/ExportDialog/index.ts new file mode 100644 index 0000000..386c71b --- /dev/null +++ b/src/components/dialogs/ExportDialog/index.ts @@ -0,0 +1 @@ +export { ExportDialog } from './ExportDialog' diff --git a/src/components/dialogs/FileSelectDialog/FileSelectDialog.tsx b/src/components/dialogs/FileSelectDialog/FileSelectDialog.tsx new file mode 100644 index 0000000..2fec1fa --- /dev/null +++ b/src/components/dialogs/FileSelectDialog/FileSelectDialog.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react' +import { Modal } from '@/components/common/Modal' +import { DialogContent } from '@/components/common/DialogContent' + +interface FileSelectDialogProps { + isOpen: boolean + onClose: () => void + onConfirm: (selected: T) => void + title: string + description: string + selectButtonText: string + selectFile: () => Promise + renderSelected: (selected: T) => React.ReactNode + errorMessage?: string | null +} + +export const FileSelectDialog = ({ + isOpen, + onClose, + onConfirm, + title, + description, + selectButtonText, + selectFile, + renderSelected, + errorMessage +}: FileSelectDialogProps) => { + const [selected, setSelected] = useState(null) + + const handleSelectFile = async () => { + const result = await selectFile() + if (result) { + setSelected(result) + } + } + + const handleConfirm = () => { + if (selected) { + onConfirm(selected) + } + } + + const handleClose = () => { + setSelected(null) + onClose() + } + + return ( + + +

+ {description} +

+ +
+ + + {selected && ( +
+ {renderSelected(selected)} +
+ )} + + {errorMessage && ( +
+ {errorMessage} +
+ )} +
+
+
+ ) +} diff --git a/src/components/dialogs/FileSelectDialog/index.ts b/src/components/dialogs/FileSelectDialog/index.ts new file mode 100644 index 0000000..eb02843 --- /dev/null +++ b/src/components/dialogs/FileSelectDialog/index.ts @@ -0,0 +1 @@ +export { FileSelectDialog } from './FileSelectDialog' diff --git a/src/components/dialogs/HTMLSelectDialog/HTMLSelectDialog.tsx b/src/components/dialogs/HTMLSelectDialog/HTMLSelectDialog.tsx new file mode 100644 index 0000000..bbc3123 --- /dev/null +++ b/src/components/dialogs/HTMLSelectDialog/HTMLSelectDialog.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react' +import { Modal } from '@/components/common/Modal' +import { DialogContent } from '@/components/common/DialogContent' +import type { LocalHtmlInfo } from '@/lib/api' + +interface HTMLSelectDialogProps { + isOpen: boolean + onClose: () => void + onLocalHtmlSelect: (info: LocalHtmlInfo) => void + errorMessage?: string | null +} + +export const HTMLSelectDialog: React.FC = ({ + isOpen, + onClose, + onLocalHtmlSelect, + errorMessage +}) => { + const [selectedInfo, setSelectedInfo] = useState(null) + + const handleSelectFile = async () => { + if (!window.electronAPI?.selectHtmlFile) return + + const result = await window.electronAPI.selectHtmlFile() + if (result.success && result.htmlPath && result.htmlDir) { + setSelectedInfo({ + htmlPath: result.htmlPath, + htmlDir: result.htmlDir, + assetsDirName: result.assetsDirName, + assetsFiles: result.assetsFiles, + }) + } + } + + const handleConfirm = () => { + if (selectedInfo) { + onLocalHtmlSelect(selectedInfo) + handleClose() + } + } + + const handleClose = () => { + setSelectedInfo(null) + onClose() + } + + const fileName = selectedInfo?.htmlPath?.replace(/\\/g, '/').split('/').pop() || '' + const assetsCount = selectedInfo?.assetsFiles?.length || 0 + + return ( + + +

+ 请选择一个 HTML 文件,系统将自动检测同目录下的资源文件夹(如 xxx_files)。 +

+ +
+ + + {selectedInfo && ( +
+
+
{fileName}
+ {selectedInfo.assetsDirName && ( +
+ 含 {assetsCount} 个资源文件 +
+ )} +
+
+ )} + + {errorMessage && ( +
+ {errorMessage} +
+ )} +
+
+
+ ) +} diff --git a/src/components/dialogs/HTMLSelectDialog/index.ts b/src/components/dialogs/HTMLSelectDialog/index.ts new file mode 100644 index 0000000..07e87c0 --- /dev/null +++ b/src/components/dialogs/HTMLSelectDialog/index.ts @@ -0,0 +1 @@ +export { HTMLSelectDialog } from './HTMLSelectDialog' diff --git a/src/components/dialogs/PDFSelectDialog/PDFSelectDialog.tsx b/src/components/dialogs/PDFSelectDialog/PDFSelectDialog.tsx new file mode 100644 index 0000000..f23ca69 --- /dev/null +++ b/src/components/dialogs/PDFSelectDialog/PDFSelectDialog.tsx @@ -0,0 +1,95 @@ +import React, { useRef, useState } from 'react' +import { Modal } from '@/components/common/Modal' +import { DialogContent } from '@/components/common/DialogContent' + +interface PDFSelectDialogProps { + isOpen: boolean + onClose: () => void + onFileSelect: (file: File) => void + errorMessage?: string | null +} + +export const PDFSelectDialog: React.FC = ({ + isOpen, + onClose, + onFileSelect, + errorMessage +}) => { + const [selectedFile, setSelectedFile] = useState(null) + const fileInputRef = useRef(null) + + const handleButtonClick = () => { + fileInputRef.current?.click() + } + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0] + if (file.type === 'application/pdf') { + setSelectedFile(file) + } else { + alert('请选择 PDF 文件') + e.target.value = '' + } + } + } + + const handleConfirm = () => { + if (selectedFile) { + onFileSelect(selectedFile) + } + } + + const handleClose = () => { + setSelectedFile(null) + onClose() + } + + return ( + + +

+ 请选择一个 PDF 文件作为文档的初始内容。 +

+ +
+ + + + + {selectedFile && ( +
+
+ {selectedFile.name} +
+
+ )} + + {errorMessage && ( +
+ {errorMessage} +
+ )} +
+
+
+ ) +} diff --git a/src/components/dialogs/PDFSelectDialog/index.ts b/src/components/dialogs/PDFSelectDialog/index.ts new file mode 100644 index 0000000..35835b0 --- /dev/null +++ b/src/components/dialogs/PDFSelectDialog/index.ts @@ -0,0 +1 @@ +export { PDFSelectDialog } from './PDFSelectDialog' diff --git a/src/components/editor/Markdown/Markdown.tsx b/src/components/editor/Markdown/Markdown.tsx new file mode 100644 index 0000000..c3b1bf2 --- /dev/null +++ b/src/components/editor/Markdown/Markdown.tsx @@ -0,0 +1,212 @@ +import { Milkdown, MilkdownProvider } from '@milkdown/react' +import { useState, useRef, useCallback, useEffect, type CSSProperties } from 'react' +import type { EditorView } from 'prosemirror-view' +import type { Node } from 'prosemirror-model' +import { useMarkdownLogic } from '@/hooks/domain/useMarkdownLogic' +import { useImageAutoDelete } from '@/hooks/domain/useImageAutoDelete' +import type { TOCItem } from '@/lib/utils' +import { useMarkdownDisplay } from '@/stores' +import { MathModal } from './MathModal' +import './styles.css' + +interface MarkdownProps { + content: string + filePath: string + onChange?: (markdown: string) => void + readOnly?: boolean + onTocUpdated?: (toc: TOCItem[]) => void +} + +interface MathModalState { + isOpen: boolean + value: string + nodePos: number | null + nodeTypeName: string | null +} + +const MarkdownInner = ({ content, filePath, onChange, readOnly = false, onTocUpdated }: MarkdownProps) => { + const containerRef = useRef(null) + const currentViewRef = useRef(null) + const { fontSize, zoom, setZoom } = useMarkdownDisplay() + const wrapperStyle = { + '--markdown-font-size': `${fontSize}px`, + } as CSSProperties + + useEffect(() => { + const container = containerRef.current + if (!container) return + + const handleWheel = (e: WheelEvent) => { + if (e.ctrlKey) { + e.preventDefault() + const delta = e.deltaY > 0 ? -10 : 10 + setZoom(zoom + delta) + } + } + + container.addEventListener('wheel', handleWheel, { passive: false }) + return () => { + container.removeEventListener('wheel', handleWheel) + } + }, [zoom, setZoom]) + + const [mathModalState, setMathModalState] = useState({ + isOpen: false, + value: '', + nodePos: null, + nodeTypeName: null + }) + + useImageAutoDelete({ content, readOnly, filePath }) + + const handleClickOn = useCallback((view: EditorView, _pos: number, node: Node, nodePos: number, event: MouseEvent) => { + if (readOnly) return false + + const typeName = node.type.name + if (typeName === 'math_block' || typeName === 'math_inline') { + const value = typeName === 'math_block' + ? (node.attrs.value as string) + : node.textContent + + currentViewRef.current = view + setMathModalState({ + isOpen: true, + value: value || '', + nodePos, + nodeTypeName: typeName + }) + event.preventDefault() + return true + } + return false + }, [readOnly]) + + const handleMathBlockCreated = useCallback((view: EditorView, _node: Node, nodePos: number) => { + if (readOnly) return + + currentViewRef.current = view + setMathModalState({ + isOpen: true, + value: '', + nodePos, + nodeTypeName: 'math_block' + }) + }, [readOnly]) + + const { loading } = useMarkdownLogic({ + content, + filePath, + readOnly, + onChange, + handleClickOn, + onMathBlockCreated: handleMathBlockCreated, + onTocUpdated + }) + + const handleMathSave = useCallback((newValue: string) => { + const { nodePos, nodeTypeName } = mathModalState + if (!currentViewRef.current || nodePos == null || !nodeTypeName) { + setMathModalState(prev => ({ ...prev, isOpen: false })) + return + } + + const view = currentViewRef.current + const { state } = view + + const currentNode = state.doc.nodeAt(nodePos) + if (!currentNode || currentNode.type.name !== nodeTypeName) { + setMathModalState(prev => ({ ...prev, isOpen: false })) + return + } + + const tr = state.tr + tr.delete(nodePos, nodePos + currentNode.nodeSize) + view.dispatch(tr) + + setTimeout(() => { + const { state: newState } = view + const tr = newState.tr + + let newNode + if (nodeTypeName === 'math_inline') { + newNode = newState.schema.nodes.math_inline.create( + null, + newState.schema.text(newValue) + ) + } else { + newNode = newState.schema.nodes.math_block.create( + { ...currentNode.attrs, value: newValue } + ) + } + + tr.insert(nodePos, newNode) + view.dispatch(tr) + view.focus() + }, 0) + + setMathModalState(prev => ({ ...prev, isOpen: false })) + }, [mathModalState]) + + const handleMathClose = useCallback(() => { + setMathModalState(prev => ({ ...prev, isOpen: false })) + }, []) + + const handleMathDelete = useCallback(() => { + const { nodePos } = mathModalState + if (!currentViewRef.current || nodePos == null) { + setMathModalState(prev => ({ ...prev, isOpen: false })) + return + } + + const view = currentViewRef.current + const { state } = view + + const currentNode = state.doc.nodeAt(nodePos) + if (!currentNode) { + setMathModalState(prev => ({ ...prev, isOpen: false })) + return + } + + const tr = state.tr + tr.delete(nodePos, nodePos + currentNode.nodeSize) + view.dispatch(tr) + view.focus() + + setMathModalState(prev => ({ ...prev, isOpen: false })) + }, [mathModalState]) + + return ( +
+ + {loading && ( +
+ )} + +
+ ) +} + +export const Markdown = ({ content, filePath, onChange, readOnly = false, onTocUpdated }: MarkdownProps) => { + return ( + + + + ) +} diff --git a/src/components/editor/Markdown/MathModal.tsx b/src/components/editor/Markdown/MathModal.tsx new file mode 100644 index 0000000..ac1f7ee --- /dev/null +++ b/src/components/editor/Markdown/MathModal.tsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect, useRef } from 'react' +import { Modal } from '@/components/common/Modal' + +interface MathModalProps { + isOpen: boolean + initialValue: string + onClose: () => void + onSave: (value: string) => void + onDelete: () => void +} + +export const MathModal = ({ isOpen, initialValue, onClose, onSave, onDelete }: MathModalProps) => { + const [value, setValue] = useState(initialValue) + const textareaRef = useRef(null) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + if (!isOpen) return + + const rafId = requestAnimationFrame(() => { + const textarea = textareaRef.current + if (!textarea) return + + try { + textarea.focus({ preventScroll: true }) + } catch { + textarea.focus() + } + + const end = textarea.value.length + textarea.setSelectionRange(end, end) + }) + + return () => { + cancelAnimationFrame(rafId) + } + }, [isOpen]) + + return ( + +
编辑 LaTeX 公式
+
+