diff --git "a/![[\346\225\260\346\215\256\350\277\201\347\247\273\346\225\231\347\250\213&\344\270\200\351\224\256\345\214\205\344\275\277\347\224\250\346\225\231\347\250\213.txt" "b/![[\346\225\260\346\215\256\350\277\201\347\247\273\346\225\231\347\250\213&\344\270\200\351\224\256\345\214\205\344\275\277\347\224\250\346\225\231\347\250\213.txt" deleted file mode 100644 index 06b7312..0000000 --- "a/![[\346\225\260\346\215\256\350\277\201\347\247\273\346\225\231\347\250\213&\344\270\200\351\224\256\345\214\205\344\275\277\347\224\250\346\225\231\347\250\213.txt" +++ /dev/null @@ -1,36 +0,0 @@ -一键包使用教程(数据迁移教程在下面): - -首先,运行 "![点我启动!!!.bat" 这个文件 -它会自动帮你寻找合适的Python环境,配置环境 -中间会安装WebUI依赖,如果默认选择的是true,直接回车就可以,如果是false,输入true回车 - - -这时, 会打开麦麦一键包控制台 -全中文操作, 看不懂我也没办法, 默认全部启动选1 - - -LPMM知识库导入向导: -https://docs.mai-mai.org/manual/deployment/LPMM.html#%E9%BA%A6%E9%BA%A6%E5%AD%A6%E4%B9%A0%E7%9F%A5%E8%AF%86 - -麦麦配置帮助,包括如何启用禁言,配置风格:https://docs.mai-mai.org/faq/maibot/settings.html - -配置文件详解:https://docs.mai-mai.org/manual/configuration/ - -插件编写指南:https://docs.mai-mai.org/develop/plugin_develop/ - - -数据迁移教程: - -从旧版一键包迁移: - -1.迁移配置文件: - -注意!!!!配置文件在更新版本时可能会有非常大的改动!! - -推荐方法:打开新旧配置文件,对照着一个一个复制 - -需要迁移的配置文件: -bot_config.toml -model_config.toml -.env - diff --git "a/![[\347\202\271\346\210\221\345\220\257\345\212\250!!!.bat" "b/![[\347\202\271\346\210\221\345\220\257\345\212\250!!!.bat" deleted file mode 100644 index bab7a51..0000000 --- "a/![[\347\202\271\346\210\221\345\220\257\345\212\250!!!.bat" +++ /dev/null @@ -1,67 +0,0 @@ -@echo off -CHCP 65001 - -setlocal enabledelayedexpansion - -chcp 65001 >nul - -REM 检测是否在压缩包内运行 -set "CURRENT_PATH=%~dp0" -echo %CURRENT_PATH% | findstr /i "temp" >nul && set "IN_ARCHIVE=1" || set "IN_ARCHIVE=0" -echo %CURRENT_PATH% | findstr /i "tmp" >nul && set "IN_ARCHIVE=1" -echo %CURRENT_PATH% | findstr /i "rar$" >nul && set "IN_ARCHIVE=1" -echo %CURRENT_PATH% | findstr /i "zip$" >nul && set "IN_ARCHIVE=1" -echo %CURRENT_PATH% | findstr /i "7z$" >nul && set "IN_ARCHIVE=1" - -if "%IN_ARCHIVE%"=="1" ( - echo - - echo ==========================================. - echo 我草,你是不是脑子有坑啊? - echo ==========================================. - echo - - echo 你™直接在压缩包里运行脚本?你是天才还是傻逼?. - echo 这种操作也就你能想得出来,孙笑川都得给你磕一个!. - echo - - echo 你™不知道解压吗?小学没毕业?. - echo - - echo 赶紧给老子滚去解压!. - echo 要不然程序出了问题,老子可不管!. - echo - - echo 操你妈的,赶紧按任意键给老子滚蛋!. - echo ==========================================. - echo - - echo 按任意键退出,然后给老子滚去解压!. - echo 以上所有文字由Gemini AI生成,如果有任何不满,请投诉Gemini谢谢. - pause >nul - exit /b 1 -) - -REM 保存当前目录 -set "CURRENT_DIR=%CD%" - -REM 使用项目自带的 Python 环境. -set "PYTHON_PATH=%~dp0runtime\python31211\bin\python.exe" - -REM 检查项目自带的 Python 是否存在. -if not exist "%PYTHON_PATH%" ( - echo 错误:找不到项目自带的 Python 环境. - echo 路径:%PYTHON_PATH%. - echo 请确认 runtime\python31211\bin\python.exe 文件存在. - pause - exit /b 1 -) - -echo 使用项目自带的 Python: %PYTHON_PATH% - -:start -REM 检查 runtime/.initialized 文件是否存在. -set "INITIALIZED_PATH=%~dp0runtime\.initialized" -if not exist "%INITIALIZED_PATH%" ( - echo 检测到 runtime/.initialized 不存在,正在执行模块更新... - "%PYTHON_PATH%" update_modules.py -) else ( - echo runtime/.initialized 存在,跳过模块更新. -) - -"%PYTHON_PATH%" main.py -pause \ No newline at end of file diff --git "a/![\345\246\202\346\236\234\344\270\200\351\224\256\345\214\205\346\227\240\351\231\220\346\233\264\346\226\260\347\232\204\350\257\235\347\202\271\346\210\221\357\274\201\357\274\201.bat" "b/![\345\246\202\346\236\234\344\270\200\351\224\256\345\214\205\346\227\240\351\231\220\346\233\264\346\226\260\347\232\204\350\257\235\347\202\271\346\210\221\357\274\201\357\274\201.bat" deleted file mode 100644 index 1e2433f..0000000 --- "a/![\345\246\202\346\236\234\344\270\200\351\224\256\345\214\205\346\227\240\351\231\220\346\233\264\346\226\260\347\232\204\350\257\235\347\202\271\346\210\221\357\274\201\357\274\201.bat" +++ /dev/null @@ -1,34 +0,0 @@ -@echo off -chcp 65001 >nul -title 创建初始化标记文件 - -echo ===================================================================== -echo 创建 .initialized 标记文件 -echo 用于手动标记一键包已初始化完成,跳过首次运行的初始化流程 -echo ===================================================================== -echo. - -REM 设置路径 -set "RUNTIME_PATH=%~dp0runtime" -set "INITIALIZED_PATH=%RUNTIME_PATH%\.initialized" - -REM 检查 runtime 目录是否存在,不存在则创建 -if not exist "%RUNTIME_PATH%" ( - echo 正在创建 runtime 目录... - mkdir "%RUNTIME_PATH%" -) - -REM 创建 .initialized 文件 -echo initialized> "%INITIALIZED_PATH%" - -if exist "%INITIALIZED_PATH%" ( - echo. - echo ✓ 已成功创建初始化标记文件: %INITIALIZED_PATH% - echo 下次启动时将跳过首次运行的初始化流程 -) else ( - echo. - echo ✗ 创建标记文件失败,请检查是否有写入权限 -) - -echo. -pause diff --git "a/![\346\233\264\346\226\260\344\270\200\351\224\256\345\214\205\344\273\223\345\272\223.bat" "b/![\346\233\264\346\226\260\344\270\200\351\224\256\345\214\205\344\273\223\345\272\223.bat" deleted file mode 100644 index 642379b..0000000 --- "a/![\346\233\264\346\226\260\344\270\200\351\224\256\345\214\205\344\273\223\345\272\223.bat" +++ /dev/null @@ -1,19 +0,0 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" - -echo ======================================== -echo 仅更新一键包仓库 -echo ======================================== -echo. -echo 正在启动一键包仓库更新... -echo. - -rem 使用内置Python运行更新脚本,只更新一键包仓库 -"runtime\python31211\bin\python.exe" update_modules.py --only-onekey - -echo. -echo ======================================== -echo 按任意键退出... -echo ======================================== -pause >nul diff --git "a/![\346\233\264\346\226\260\346\211\200\346\234\211\346\250\241\345\235\227.bat" "b/![\346\233\264\346\226\260\346\211\200\346\234\211\346\250\241\345\235\227.bat" deleted file mode 100644 index 3b6a570..0000000 --- "a/![\346\233\264\346\226\260\346\211\200\346\234\211\346\250\241\345\235\227.bat" +++ /dev/null @@ -1,35 +0,0 @@ -@echo off -chcp 65001 >nul -title 更新所有模块 - -echo ===================================================================== -echo 模块更新工具 (提示:如果出现更新失败的错误,请尝试右键管理员运行本脚本!) -echo ===================================================================== -echo. - -REM 设置Python路径 -set "PYTHON_PATH=%~dp0runtime\python31211\bin\python.exe" - -REM 检查Python是否存在 -if exist "%PYTHON_PATH%" ( - echo 使用内置Python: %PYTHON_PATH% -) else ( - echo 错误:未找到内置Python. - echo 路径: %PYTHON_PATH% - pause - exit /b 1 -) -echo. - -REM 运行更新脚本 -echo 开始执行更新脚本... -"%PYTHON_PATH%" "%~dp0\update_modules.py" - -echo. -if %errorlevel% equ 0 ( - echo 更新完成!. -) else ( - echo 更新过程中出现错误!. -) -echo 按任意键退出... -pause >nul diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1b74cac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - master + - desktop + +jobs: + electron-build: + name: Typecheck and build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build Electron app + run: bun run build diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml new file mode 100644 index 0000000..4f7418b --- /dev/null +++ b/.github/workflows/release-windows.yml @@ -0,0 +1,131 @@ +name: Windows Release + +on: + workflow_dispatch: + inputs: + payload_url: + description: Zip URL containing runtime/ and modules/. runtime/python and runtime/git are required for the full installer. + required: false + type: string + payload_sha256: + description: Optional SHA-256 for the payload zip. + required: false + type: string + tag_name: + description: Optional tag to publish as a draft GitHub release, for example v0.1.0. + required: false + type: string + create_github_release: + description: Create a draft GitHub release from the generated installer. + required: true + default: false + type: boolean + prerelease: + description: Mark the GitHub release as prerelease. + required: true + default: false + type: boolean + +permissions: + contents: write + +jobs: + windows-installer: + name: Build Windows x64 installer + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Validate GitHub release input + if: ${{ inputs.create_github_release && inputs.tag_name == '' }} + shell: pwsh + run: throw "tag_name is required when create_github_release is true." + + - name: Download release payload + if: ${{ inputs.payload_url != '' }} + shell: pwsh + run: | + Invoke-WebRequest -Uri "${{ inputs.payload_url }}" -OutFile payload.zip + + if ("${{ inputs.payload_sha256 }}" -ne "") { + $actual = (Get-FileHash payload.zip -Algorithm SHA256).Hash.ToLowerInvariant() + $expected = "${{ inputs.payload_sha256 }}".ToLowerInvariant() + if ($actual -ne $expected) { + throw "Payload SHA-256 mismatch. Expected $expected, got $actual." + } + } + + New-Item -ItemType Directory -Force -Path .payload | Out-Null + Expand-Archive payload.zip -DestinationPath .payload -Force + + $payloadRoot = Resolve-Path .payload + if (!(Test-Path (Join-Path $payloadRoot "runtime")) -or !(Test-Path (Join-Path $payloadRoot "modules"))) { + $candidate = Get-ChildItem .payload -Directory | Where-Object { + (Test-Path (Join-Path $_.FullName "runtime")) -and (Test-Path (Join-Path $_.FullName "modules")) + } | Select-Object -First 1 + + if ($null -eq $candidate) { + throw "Payload zip must contain runtime/ and modules/ at its root or one directory below the root." + } + + $payloadRoot = $candidate.FullName + } + + if (Test-Path runtime) { + Remove-Item runtime -Recurse -Force + } + if (Test-Path modules) { + Remove-Item modules -Recurse -Force + } + + Move-Item (Join-Path $payloadRoot "runtime") runtime + Move-Item (Join-Path $payloadRoot "modules") modules + + - name: Check release payload + run: bun run release:check + + - name: Build installers + run: bun run release:win + + - name: Upload installer artifacts + uses: actions/upload-artifact@v4 + with: + name: maibot-onekey-windows-x64 + path: | + release/*.exe + release/*.blockmap + release/*.yml + if-no-files-found: error + + - name: Create draft GitHub release + if: ${{ inputs.create_github_release && inputs.tag_name != '' }} + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ inputs.tag_name }} + shell: pwsh + run: | + $assets = Get-ChildItem release -File | Where-Object { + $_.Name -match '\.(exe|blockmap|yml)$' + } | Select-Object -ExpandProperty FullName + + if ($assets.Count -eq 0) { + throw "No release assets found." + } + + $args = @("release", "create", $env:TAG_NAME) + $args += $assets + $args += @("--title", $env:TAG_NAME, "--generate-notes", "--draft") + + if ("${{ inputs.prerelease }}" -eq "true") { + $args += "--prerelease" + } + + gh @args diff --git a/.gitignore b/.gitignore index 0914849..320f3d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# macOS +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -6,7 +9,13 @@ __pycache__/ # C extensions *.so - +/node_modules/ +/out/ +/release/ +/release-assets/ +/.payload/ +/payload.zip +/release-payload*.zip MaiBot/ modules/ @@ -20,7 +29,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ @@ -197,4 +205,4 @@ cython_debug/ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data # refer to https://docs.cursor.com/context/ignore-files .cursorignore -.cursorindexingignore \ No newline at end of file +.cursorindexingignore diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a72ad4a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,304 @@ +# Changelog + +## 0.3.3 - 2026-05-19 + +### 内置模块 +- 更新内置 MaiBot 到 main 最新提交。 + +### 随便聊聊 +- 优化planner显示效果 +- 修复无法识别图片的问题 +- 支持文件,表情包和语音 + +### 启动器 +- 允许自定义配色 +- 优化首页显示,并显示LLM统计信息 +- 可以点击右下角麦麦进入悬浮模式 + +### 终端 +- 允许调节字体大小 +- 终端内的超链接可点击 + +### 适配器配置 +- MaiBot Core 启动时不再同步/重写 NapCat、SnowLuma 适配器配置,避免启动时覆盖用户配置。 + + +## 0.3.2 - 2026-05-18 + +### 内置模块 + +- 更新内置 MaiBot 到 main 最新提交,并同步 NapCat 适配器 v1.1.0、SnowLuma 适配器 v0.3.0 与 SnowLuma 1.8.5。 + +### QQ 后端与适配器配置 + +- 未完成 NapCat / SnowLuma 初始化时,顶部服务栏和首页不再显示 QQ 后端标签/配置卡片。 +- 初始化 NapCat / SnowLuma 时自动弹出对应适配器配置窗口,方便配置聊天黑白名单和允许群聊范围。 +- 适配器配置弹窗优先显示“聊天过滤”,隐藏 `plugin`、`filters` 及内部名单过滤开关,隐藏标题说明里的插件 ID / 配置路径,并将底部操作合并为“保存并关闭”。 +- 首页 QQ 后端卡片支持自定义 WebUI 打开端口。 + +### 随便聊聊 + +- 首页新增随便聊聊快捷入口,并可跳转到独立随便聊聊标签页。 +- planner 信息支持流式显示、1 秒后自动折叠、按工具调用分块展示,并提供显示开关; + +### 插件管理 + +- 插件市场和插件更新源跟随模块更新源配置。 +- 插件更新改为 Git 强制拉取并 reset 到远端目标,失败时恢复到更新前提交和原远端配置。 +- 插件配置优先读取 MaiBot WebUI / Dashboard 运行时配置接口,支持 schema、raw、保存接口和多语言字段信息。 + +### 终端与日志 + +- 终端启用换行转换并优化 resize / fit 时机,缓解长日志路径导致的错行和错位。 +- 终端顶部按钮、服务标签和底部状态栏进一步压缩,减少内容区被挤占。 + + +## 0.3.1 - 2026-05-17 + +设置中心新增两个启动器重置操作:可仅清空启动器设置并重新进入启动引导,也可在停止全部服务后完整清空运行时资源目录,将 MaiBot、NapCat、SnowLuma、Python 覆盖依赖、日志与启动器配置一并还原到初始状态。 +消息平台接入状态改为由启动器显式记录,不再通过 `bot_config.toml` 中是否已有 QQ 账号推断,避免新安装或模板配置导致首页跳过“连接到消息软件平台.......”入口。 +切换 QQ 后端时会自动准备对应后端的运行时目录;首次从 NapCat 切到 SnowLuma 时会从内置模板补齐 SnowLuma 程序文件,但不会覆盖已有 config、data、logs。 +Windows 打包流程拆分为 `basic` 与 `full` 两个安装包:`basic` 保持基础 Python 干净,`full` 会随包携带预装 MaiBot、Dashboard 与本地聊天依赖的 `python-overrides`,首次初始化时复制到运行时资源目录。 + +首页 +重排首页为左侧功能卡片、右侧统计信息的布局,统一展示 MaiBot Core、QQ 后端和插件入口。 +精简首页顶部与卡片文案,隐藏服务 URL、重复标题和冗余概览栏。 +MaiBot Core 卡片合并本地版本、最新版本与更新入口,并支持在更新时选择模块更新源。 +QQ 后端卡片增加适配器插件设置入口,会根据当前后端自动打开 NapCat 或 SnowLuma 适配器配置。 +插件入口合并商店与管理操作,并将按钮改为纵向排列。 +首页物理形象改为固定在窗口右下角,不再占用首页网格空间。 + +首次启动引导 +首次引导改为自动启动 MaiBot Core,不再要求用户手动点击初始化。 +初始化阶段隐藏 NapCat、SnowLuma 和 QQ 后端配置,引导用户先完成 MaiBot Core 基础初始化。 +新增 Python 依赖源选择,支持清华源、官方 PyPI、阿里源,并会记住选择。 +引导弹窗展示依赖安装进度和相关日志。 +首次引导会等待 MaiBot WebUI 端口真正就绪后再自动进入 MaiBot 页面。 +修正“进程已启动但 WebUI 未就绪”时误显示完成的问题。 + +窗口与设置 +新增关闭行为选择,支持退出应用或最小化到托盘,并可记住首次选择。 +设置中心新增“通用”分区,可调整关闭行为、终端模式等通用偏好。 +关闭到托盘时改为隐藏窗口,托盘菜单继续提供显示、启动全部、停止全部和退出入口。 + +插件与快捷入口 +插件页默认打开后显示“管理”而不是“商店”。 +支持从首页直接跳转到指定已安装插件并打开配置弹窗。 +快捷操作页移除标题下方说明文字,页面更紧凑。 + +终端与服务显示 +顶部服务栏隐藏总体运行计数和服务端口,保留单服务控制按钮。 +终端页压缩标题栏、服务切换栏和底部状态栏高度,为 PTY 内容区释放更多空间。 +修复部分服务状态和关闭弹窗文案的中文乱码。 + +打包与安装 +Windows NSIS 安装流程改为在安装目录同盘创建 staging 目录并通过目录重命名提交,减少大量小文件逐个 CopyFiles 的安装开销。 +修复同盘 staging 安装方式中空安装目录、目录锁定、旧目录恢复和 NSIS 未使用标签导致的打包问题。 +NSIS patch 脚本增强幂等与旧模板迁移能力。 + +设置中心新增重置 SnowLuma 组件入口,可在停止服务后清空 SnowLuma 目录及配置,并从一键包内置模板重新复制。 +服务启动流程不再自动从内置模板补全 NapCat / SnowLuma 模块,模板复制收敛到首次初始化和“准备基础目录”操作,避免启动时影响用户手动修改的后端目录。 +初始化与修复 MaiBot 配置时会固定写入 `onekey-local-chat:onekey-local-bot`,确保一键包本地聊天平台拥有独立机器人账号。 +首页未配置 QQ 账号时,QQ 后端卡片改为“连接到消息软件平台.......”入口,可选择 QQ-NapCat 或 QQ-SnowLuma,自动写入对应适配器与 WebSocket 配置并启动后端。 + +## 0.3.0 - 2026-05-16 + +首页 +新增运行总览,集中展示服务状态、版本、健康状态和快捷入口。 +首页会根据当前 QQ 后端显示 NapCat 或 SnowLuma。 +新增首页物理掉落彩蛋。 + +QQ 后端 +新增 SnowLuma 启动方式。 +支持 NapCat / SnowLuma 一键切换。 + +随便聊聊 +新增本地聊天功能。 +支持保存本地用户名。支持自定义用户头像和 bot 头像。支持发送图片。 + +插件 +新增插件商店和插件管理。 +插件商店显示下载量、评分、点赞等信息。 +插件详情支持渲染 README 和查看评论。 +插件管理支持启用、禁用、配置、更新、卸载插件。 +插件状态用灰灯、红灯、绿灯表示未启用、加载失败、加载成功。 + +设置中心 +账号配置支持选择 NapCat / SnowLuma。 +支持导入 MaiBot 数据和配置文件。 +支持重置 MaiBot 数据。 +支持管理实例路径、Python/Git 运行时和 Python 依赖。 + +终端与服务 +终端改为稳定暗色显示。 +后台 MaiBot Core 终端显示稳定性提升。 +服务启动、停止、端口冲突提示更清晰。 + + +## 0.2.2 - 2026-05-13 + +本版重点:调整 Windows 打包与运行时策略,内置 Python 改为精简基础环境;新增自定义 Python 路径、实例路径管理和终端模式;优化模块更新、环境检查、插件管理与首页展示。 + +### 打包与发布 + +- Windows 安装包拆分为 `full` 与 `lite` 变体。 +- `full` 包包含精简内置 Python 与内置 Git。 +- `lite` 包仍包含精简内置 Python,但不包含内置 Git,运行时会寻找系统 Git。 +- 打包检查会校验内置 Python 是否保持精简,避免把业务依赖打进基础 Python。 +- 新增并调整 `release:win`、`release:win:full`、`release:win:lite` 等发布脚本。 + +### Python 运行时 + +- 内置 Python 仅作为基础环境,不再预装大量 MaiBot 业务依赖。 +- MaiBot Core 默认使用“基础 Python + 用户可写覆盖层”的方式启动。 +- 新增“自定义 Python 路径”选项,开启后可手动输入、浏览选择或从系统 Python 下拉候选中选择。 +- 使用自定义 Python 时,不再使用内置 Python 与覆盖层逻辑,也不再注入 Python 覆盖依赖。 +- “Python 覆盖依赖”界面文案改为“手动更新Python 依赖”。 +- 手动依赖更新仅维护 `maibot-dashboard` 与 `maim-message`。 + +### 环境检查 + +- 合并 Git 检查项,不再区分“Git 运行时”和“Git 可执行文件”。 +- 移除“内置 modules 模板”和“机器人 QQ 号”等不适合作为环境依赖的检查项。 +- 修复多处环境检查、首页与设置页中的中文乱码显示。 +- Python、Git 缺失或版本不满足要求时,会给出更明确的提示。 + +### 实例路径 + +- 新增实例路径管理,可迁移或切换 MaiBot、NapCat 等可写资源目录。 +- 基础 Python 位置与 Python 覆盖层位置固定,不再允许在实例路径中修改。 +- MaiBot 与 NapCat 的资源路径调整会在服务停止后执行,避免运行中切换造成状态错乱。 + +### 模块更新 + +- MaiBot 更新失败时不再回退到一键包内置版本,而是恢复到更新前的提交与原始 `origin`。 +- 子模块更新失败时同样会恢复到更新前状态。 +- 首页与设置页的 MaiBot 更新逻辑保持一致。 +- 更新源与 MaiBot 仓库配置整合进 MaiBot Core 更新卡片。 +- 移除“远程拉取失败会回退到内置快照”的旧提示。 + +### 首页与界面 + +- 修复首页和设置中心多处文本乱码。 +- 首页右上角移除 MaiBot / NapCat 快捷按钮。 +- 首页保留服务状态、端口健康、一键包版本和 MaiBot 本地版本等核心信息。 +- 设置页的 Python 路径输入改为可输入、可下拉、可浏览选择的组合体验。 + +### 终端与服务 + +- 新增终端模式设置,可选择内嵌终端或外部 Windows 终端。 +- 服务状态会显示内嵌或外部终端的 PID 信息。 +- 服务启动时会根据自定义 Python 状态决定是否注入覆盖层。 + +### 插件管理 + +- 插件管理支持读取、渲染并保存插件 `config.toml`。 +- 支持字符串、数字、布尔值、数组、对象等常见配置类型。 +- 移除旧的 napcat-adapter 独立配置卡片,统一通过插件管理维护。 + +## 0.2.1 - 2026-05-13 + +本版重点:优化首页、模块更新、启动依赖检查与运行日志体验,并补充 Windows 打包资源检查。 + +### 首页 + +- 新增首页,展示服务运行数量、端口健康、一键包版本、MaiBot 版本和 Dashboard 版本。 +- 首页提供 MaiBot Core 和 WebUI 更新入口。 +- 首页布局调整为更紧凑的工具界面信息密度。 +- 远端版本读取改为后台刷新,减少首页打开时的等待。 + +### MaiBot 更新 + +- 新增模块更新源配置,可在 GitHub 镜像代理、官方 GitHub 和自定义源之间切换。 +- MaiBot 更新支持读取远端 tag,并可选择正式版、测试版或旧版目标版本。 +- 移除 MaiBot 更新流程中的 napcat-adapter 独立更新逻辑。 +- 移除 napcat-adapter 专用修复入口,模块更新聚焦 MaiBot 主模块。 + +### 启动依赖 + +- MaiBot Core 启动前会检查并安装 MaiBot 声明依赖到 Python 覆盖目录。 +- 启动依赖安装会写入服务系统日志。 +- MaiBot 运行环境注入 `PYTHONPATH`,优先加载覆盖层依赖。 +- 依赖更新从“直接全量安装”优化为先检测 requirements / pyproject 依赖是否满足。 +- 支持读取 `pyproject.toml` 的项目依赖。 +- 依赖更新支持流式输出和取消;启动过程中停止 MaiBot 会中断正在运行的依赖安装进程。 +- pip 安装加入 `--no-compile`,减少安装阶段不必要的编译和长时间无反馈。 + +### 终端与日志 + +- 终端页会展示启动前依赖更新、服务启动状态等系统日志。 +- 附加已有终端会补写最近系统日志,并避免重复写入。 +- PTY IPC 容错增强:会话缺失时读取缓冲区返回空内容,`resize` 不再抛出到前端。 +- 内嵌 xterm 终端固定使用深色控制台主题,不再跟随应用主题切换。 + +### 打包与资源检查 + +- 打包配置改为 `compression: store`。 +- Windows payload 检查加入内置 napcat-adapter 插件要求。 +- payload 检查增强对 NapCat 版本资源的识别能力。 + +## 0.2.0 - 2026-05-12 + +本版重点:加入 MaiBot 插件市场和已安装插件管理,并增强 NapCat 新目录结构下的兼容性。 + +### 插件市场与插件管理 + +- 新增 MaiBot 插件市场。 +- 新增已安装插件管理页面。 +- 支持插件安装、更新、卸载、搜索和操作确认。 +- 支持展示插件版本、作者、分类、描述和仓库信息。 +- 根据本地 MaiBot 版本判断插件 manifest 兼容性,并在不兼容时给出提示。 + +### NapCat 兼容性 + +- NapCat 启动优先使用 `node.exe index.js`,找不到时再回退到 Windows 启动器或 `NapCatWinBootMain.exe`。 +- NapCat 配置写入兼容新的目录结构,包括 `napcat/config` 和版本化资源目录。 +- 打包资源过滤补充排除 NapCat WebUI 配置,避免把运行时登录配置带入安装包。 +- Windows payload 检查增强,要求能定位实际 NapCat 运行资源。 + +## 0.1.10 - 2026-05-09 至 2026-05-11 + +本版重点:新增首页、模块更新源、启动依赖检查和服务日志能力,并优化打包资源检查。 + +### 首页 + +- 新增首页,展示服务运行数量、端口健康、一键包版本、MaiBot 版本和 Dashboard 版本。 +- 首页提供 MaiBot / NapCat WebUI 快捷入口。 +- 首页新增 MaiBot Core 和 WebUI 更新入口。 +- 首页布局调整为更紧凑的工具界面信息密度。 +- 远端版本读取改为后台刷新,减少首页打开时的等待。 + +### MaiBot 更新 + +- 新增模块更新源配置,可在 GitHub 镜像代理、官方 GitHub 和自定义源之间切换。 +- MaiBot 更新支持读取远端 tag,并可选择正式版、测试版或旧版目标版本。 +- 远端同步失败时可回退到一键包内置快照。 +- 移除 MaiBot 更新流程中的 napcat-adapter 独立更新逻辑。 +- 移除 napcat-adapter 专用修复入口,模块更新聚焦 MaiBot 主模块。 + +### 启动依赖 + +- MaiBot Core 启动前会检查并安装 MaiBot 声明依赖到 Python 覆盖目录。 +- 启动依赖安装会写入服务系统日志。 +- MaiBot 运行环境注入 `PYTHONPATH`,优先加载覆盖层依赖。 +- 依赖更新从“直接全量安装”优化为先检测 requirements / pyproject 依赖是否满足。 +- 支持读取 `pyproject.toml` 的项目依赖。 +- 依赖更新支持流式输出和取消;启动过程中停止 MaiBot 会中断正在运行的依赖安装进程。 +- pip 安装加入 `--no-compile`,减少安装阶段不必要的编译和长时间无反馈。 + +### 终端与日志 + +- 终端页会展示启动前依赖更新、服务启动状态等系统日志。 +- 附加已有终端会补写最近系统日志,并避免重复写入。 +- PTY IPC 容错增强:会话缺失时读取缓冲区返回空内容,`resize` 不再抛出到前端。 +- 内嵌 xterm 终端固定使用深色控制台主题,不再跟随应用主题切换。 + +### 打包与资源检查 + +- 打包配置改为 `compression: store`。 +- Windows payload 检查加入内置 napcat-adapter 插件要求。 +- payload 检查增强对 NapCat 版本资源的识别能力。 + +### 清理 + +- 删除旧的数据迁移说明文本文件。 diff --git a/README.md b/README.md index ce392e4..156dd8b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ -# MaiBotOneKey -麦麦MaiBot一键包附加脚本存放仓库 +# MaiBot OneKey Desktop + +MaiBot OneKey 的 Electron 桌面壳。当前桌面版负责初始化检查、服务启动/停止、单安装目录单实例、日志/状态展示,以及 MaiBot WebUI、NapCat WebUI、PTY 终端、设置状态页的统一入口。 + +旧的 `.bat` 和根目录 Python 启动入口已经清理,普通用户入口统一为 Windows 安装包。 + +## 开发 + +```bash +bun install +bun run dev +``` + +常用检查: + +```bash +bun run typecheck +bun run build +``` + +## 运行时资源 + +打包版默认把可写运行资源放在 `%APPDATA%\MaiBotOneKeyDesktop\<安装目录hash>` 下;设置中心的「实例路径」页可以迁移运行时资源目录。迁移只移动 `modules/` 与 `python-overrides/`,日志、实例锁和一键包设置仍保留在用户数据目录。 + +## Windows 打包 + +Windows x64 NSIS 安装包会同时产出两个变体:`full` 完整包包含内置 Python 与 Git,`lite` 精简包不包含内置 Python 与 Git,会在运行时自动寻找系统 Python 3.12+ 与系统 Git。打包前需要在仓库根目录放好完整 payload: + +```text +runtime/ + python/ + python.exe + DLLs/ + Lib/ + Scripts/pip.exe + git/ + bin/git.exe +modules/ + MaiBot/ + MaiBot-Napcat-Adapter/ + napcat/ +``` + +只构建 `lite` 变体时,`runtime/python/` 与 `runtime/git/` 可以省略: + +```bash +bun run release:win:lite +``` + +发布前检查: + +```bash +bun run release:check +``` + +生成两个安装包: + +```bash +bun run release:win +``` + +产物输出到 `release/`,文件名会带上 `full` 或 `lite` 后缀。`runtime/` 和 `modules/` 会作为 `extraResources` 放进完整包;`lite` 变体会排除 `runtime/python/` 与 `runtime/git/`,缺失时会在环境检查中提供 Python 和 Git 下载入口。 + +## CI + +- `.github/workflows/ci.yml`:在 Linux、macOS、Windows 上执行依赖安装、类型检查和 Electron 构建,不需要 release payload。 +- `.github/workflows/release-windows.yml`:手动触发 Windows x64 安装包构建,可输入 payload zip URL;构建完整包时 zip 内需要包含 `runtime/` 和 `modules/`。 + +更多发布细节见 [docs/release.md](docs/release.md)。 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..0794153 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1188 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "maibot-onekey-desktop", + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tanstack/react-virtual": "^3.13.24", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "iconv-lite": "^0.7.2", + "lucide-react": "^0.468.0", + "next-themes": "^0.4.6", + "node-pty": "^1.1.0", + "radix-ui": "^1.4.3", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "smol-toml": "^1.6.1", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "ws": "^8.20.1", + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.13", + "@types/bun": "^1.2.23", + "@types/node": "^22.15.34", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@types/ws": "^8.18.1", + "@vitejs/plugin-react": "^5.0.3", + "electron": "^38.1.2", + "electron-builder": "^26.0.12", + "electron-vite": "^4.0.1", + "tailwindcss": "^4.1.13", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.9.3", + "vite": "^7.1.7", + }, + }, + }, + "packages": { + "7zip-bin": ["7zip-bin@5.2.0", "https://registry.npmmirror.com/7zip-bin/-/7zip-bin-5.2.0.tgz", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", { "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" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", { "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" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", { "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" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.28.6", "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", { "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" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + + "@electron/asar": ["@electron/asar@3.4.1", "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], + + "@electron/fuses": ["@electron/fuses@1.8.0", "https://registry.npmmirror.com/@electron/fuses/-/fuses-1.8.0.tgz", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], + + "@electron/get": ["@electron/get@2.0.3", "https://registry.npmmirror.com/@electron/get/-/get-2.0.3.tgz", { "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" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + + "@electron/notarize": ["@electron/notarize@2.5.0", "https://registry.npmmirror.com/@electron/notarize/-/notarize-2.5.0.tgz", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + + "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "https://registry.npmmirror.com/@electron/osx-sign/-/osx-sign-1.3.3.tgz", { "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" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], + + "@electron/rebuild": ["@electron/rebuild@4.0.4", "https://registry.npmmirror.com/@electron/rebuild/-/rebuild-4.0.4.tgz", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="], + + "@electron/universal": ["@electron/universal@2.0.3", "https://registry.npmmirror.com/@electron/universal/-/universal-2.0.3.tgz", { "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" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], + + "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "https://registry.npmmirror.com/@electron/windows-sign/-/windows-sign-1.2.2.tgz", { "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" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], + + "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "https://registry.npmmirror.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "https://registry.npmmirror.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "https://registry.npmmirror.com/@radix-ui/react-form/-/react-form-0.1.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "https://registry.npmmirror.com/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "https://registry.npmmirror.com/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="], + + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "https://registry.npmmirror.com/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], + + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], + + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.2.4.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.2.4.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.2.4.tgz", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], + + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.24", "https://registry.npmmirror.com/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz", { "dependencies": { "@tanstack/virtual-core": "3.14.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg=="], + + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/bun": ["@types/bun@1.3.13", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.13.tgz", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + + "@types/debug": ["@types/debug@4.1.13", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/fs-extra": ["@types/fs-extra@9.0.13", "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + + "@types/keyv": ["@types/keyv@3.1.4", "https://registry.npmmirror.com/@types/keyv/-/keyv-3.1.4.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + + "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@22.19.17", "https://registry.npmmirror.com/@types/node/-/node-22.19.17.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + + "@types/plist": ["@types/plist@3.0.5", "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + + "@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/responselike": ["@types/responselike@1.0.3", "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + + "@types/verror": ["@types/verror@1.10.11", "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@types/yauzl": ["@types/yauzl@2.10.3", "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + + "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "https://registry.npmmirror.com/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], + + "@xterm/xterm": ["@xterm/xterm@6.0.0", "https://registry.npmmirror.com/@xterm/xterm/-/xterm-6.0.0.tgz", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], + + "abbrev": ["abbrev@4.0.0", "https://registry.npmmirror.com/abbrev/-/abbrev-4.0.0.tgz", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], + + "agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "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" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ajv-keywords": ["ajv-keywords@3.5.2", "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], + + "app-builder-lib": ["app-builder-lib@26.8.1", "https://registry.npmmirror.com/app-builder-lib/-/app-builder-lib-26.8.1.tgz", { "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.8.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.8.1", "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" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], + + "argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "assert-plus": ["assert-plus@1.0.0", "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "astral-regex": ["astral-regex@2.0.0", "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "async": ["async@3.2.6", "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-exit-hook": ["async-exit-hook@2.0.1", "https://registry.npmmirror.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + + "asynckit": ["asynckit@0.4.0", "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "at-least-node": ["at-least-node@1.0.0", "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.23", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g=="], + + "boolean": ["boolean@3.2.0", "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + + "brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + + "browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "buffer": ["buffer@5.7.1", "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "builder-util": ["builder-util@26.8.1", "https://registry.npmmirror.com/builder-util/-/builder-util-26.8.1.tgz", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "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" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], + + "builder-util-runtime": ["builder-util-runtime@9.5.1", "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], + + "bun-types": ["bun-types@1.3.13", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + + "cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "cacheable-lookup": ["cacheable-lookup@5.0.4", "https://registry.npmmirror.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], + + "cacheable-request": ["cacheable-request@7.0.4", "https://registry.npmmirror.com/cacheable-request/-/cacheable-request-7.0.4.tgz", { "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" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001791", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + + "chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chownr": ["chownr@3.0.0", "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], + + "ci-info": ["ci-info@4.4.0", "https://registry.npmmirror.com/ci-info/-/ci-info-4.4.0.tgz", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "cli-truncate": ["cli-truncate@2.1.0", "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + + "cliui": ["cliui@8.0.1", "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone-response": ["clone-response@1.0.3", "https://registry.npmmirror.com/clone-response/-/clone-response-1.0.3.tgz", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + + "clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@5.1.0", "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "compare-version": ["compare-version@0.1.2", "https://registry.npmmirror.com/compare-version/-/compare-version-0.1.2.tgz", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + + "concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "core-util-is": ["core-util-is@1.0.2", "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + + "crc": ["crc@3.8.0", "https://registry.npmmirror.com/crc/-/crc-3.8.0.tgz", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], + + "cross-dirname": ["cross-dirname@0.1.0", "https://registry.npmmirror.com/cross-dirname/-/cross-dirname-0.1.0.tgz", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], + + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + + "define-data-property": ["define-data-property@1.1.4", "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node": ["detect-node@2.1.0", "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + + "detect-node-es": ["detect-node-es@1.1.0", "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "dir-compare": ["dir-compare@4.2.0", "https://registry.npmmirror.com/dir-compare/-/dir-compare-4.2.0.tgz", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + + "dmg-builder": ["dmg-builder@26.8.1", "https://registry.npmmirror.com/dmg-builder/-/dmg-builder-26.8.1.tgz", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="], + + "dmg-license": ["dmg-license@1.0.11", "https://registry.npmmirror.com/dmg-license/-/dmg-license-1.0.11.tgz", { "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" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + + "dotenv": ["dotenv@16.6.1", "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dotenv-expand": ["dotenv-expand@11.0.7", "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-11.0.7.tgz", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ejs": ["ejs@3.1.10", "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "electron": ["electron@38.8.6", "https://registry.npmmirror.com/electron/-/electron-38.8.6.tgz", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg=="], + + "electron-builder": ["electron-builder@26.8.1", "https://registry.npmmirror.com/electron-builder/-/electron-builder-26.8.1.tgz", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "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" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "https://registry.npmmirror.com/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="], + + "electron-publish": ["electron-publish@26.8.1", "https://registry.npmmirror.com/electron-publish/-/electron-publish-26.8.1.tgz", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.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" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.344", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], + + "electron-vite": ["electron-vite@4.0.1", "https://registry.npmmirror.com/electron-vite/-/electron-vite-4.0.1.tgz", { "dependencies": { "@babel/core": "^7.27.7", "@babel/plugin-transform-arrow-functions": "^7.27.1", "cac": "^6.7.14", "esbuild": "^0.25.5", "magic-string": "^0.30.17", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-QqacJbA8f1pmwUTqki1qLL5vIBaOQmeq13CZZefZ3r3vKVaIoC7cpoTgE+KPKxJDFTax+iFZV0VYvLVWPiQ8Aw=="], + + "electron-winstaller": ["electron-winstaller@5.4.0", "https://registry.npmmirror.com/electron-winstaller/-/electron-winstaller-5.4.0.tgz", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "end-of-stream": ["end-of-stream@1.4.5", "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + + "env-paths": ["env-paths@2.2.1", "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "err-code": ["err-code@2.0.3", "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es6-error": ["es6-error@4.1.1", "https://registry.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + + "esbuild": ["esbuild@0.25.12", "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "extract-zip": ["extract-zip@2.0.1", "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "extsprintf": ["extsprintf@1.4.1", "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "filelist": ["filelist@1.0.6", "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], + + "form-data": ["form-data@4.0.5", "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", { "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" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fs-extra": ["fs-extra@10.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "fs.realpath": ["fs.realpath@1.0.0", "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@5.2.0", "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "glob": ["glob@7.2.3", "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "global-agent": ["global-agent@3.0.0", "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz", { "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" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + + "globalthis": ["globalthis@1.0.4", "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "got": ["got@11.8.6", "https://registry.npmmirror.com/got/-/got-11.8.6.tgz", { "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" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], + + "graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hosted-git-info": ["hosted-git-info@4.1.0", "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "http2-wrapper": ["http2-wrapper@1.0.3", "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "https://registry.npmmirror.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inflight": ["inflight@1.0.6", "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "isbinaryfile": ["isbinaryfile@5.0.7", "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.7.tgz", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + + "isexe": ["isexe@3.1.5", "https://registry.npmmirror.com/isexe/-/isexe-3.1.5.tgz", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "jake": ["jake@10.9.4", "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + + "jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.1", "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "lazy-val": ["lazy-val@1.0.5", "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], + + "lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lowercase-keys": ["lowercase-keys@2.0.0", "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], + + "lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.468.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.468.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA=="], + + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "matcher": ["matcher@3.0.0", "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime": ["mime@2.6.0", "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-response": ["mimic-response@3.1.0", "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "https://registry.npmmirror.com/minizlib/-/minizlib-3.1.0.tgz", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mkdirp": ["mkdirp@0.5.6", "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next-themes": ["next-themes@0.4.6", "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + + "node-abi": ["node-abi@4.28.0", "https://registry.npmmirror.com/node-abi/-/node-abi-4.28.0.tgz", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], + + "node-addon-api": ["node-addon-api@7.1.1", "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-api-version": ["node-api-version@0.2.1", "https://registry.npmmirror.com/node-api-version/-/node-api-version-0.2.1.tgz", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + + "node-gyp": ["node-gyp@12.3.0", "https://registry.npmmirror.com/node-gyp/-/node-gyp-12.3.0.tgz", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="], + + "node-pty": ["node-pty@1.1.0", "https://registry.npmmirror.com/node-pty/-/node-pty-1.1.0.tgz", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], + + "node-releases": ["node-releases@2.0.38", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + + "nopt": ["nopt@9.0.0", "https://registry.npmmirror.com/nopt/-/nopt-9.0.0.tgz", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], + + "normalize-url": ["normalize-url@6.1.0", "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + + "object-keys": ["object-keys@1.1.1", "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "p-cancelable": ["p-cancelable@2.1.1", "https://registry.npmmirror.com/p-cancelable/-/p-cancelable-2.1.1.tgz", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + + "p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pe-library": ["pe-library@0.4.1", "https://registry.npmmirror.com/pe-library/-/pe-library-0.4.1.tgz", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + + "pend": ["pend@1.2.0", "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "plist": ["plist@3.1.0", "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + + "postcss": ["postcss@8.5.12", "https://registry.npmmirror.com/postcss/-/postcss-8.5.12.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="], + + "postject": ["postject@1.0.0-alpha.6", "https://registry.npmmirror.com/postject/-/postject-1.0.0-alpha.6.tgz", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + + "proc-log": ["proc-log@6.1.0", "https://registry.npmmirror.com/proc-log/-/proc-log-6.1.0.tgz", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + + "progress": ["progress@2.0.3", "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise-retry": ["promise-retry@2.0.1", "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "https://registry.npmmirror.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "pump": ["pump@3.0.4", "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quick-lru": ["quick-lru@5.1.1", "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "radix-ui": ["radix-ui@1.4.3", "https://registry.npmmirror.com/radix-ui/-/radix-ui-1.4.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], + + "react": ["react@19.2.5", "https://registry.npmmirror.com/react/-/react-19.2.5.tgz", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + + "react-dom": ["react-dom@19.2.5", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.5.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + + "react-refresh": ["react-refresh@0.18.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + + "require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resedit": ["resedit@1.7.2", "https://registry.npmmirror.com/resedit/-/resedit-1.7.2.tgz", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + + "resolve-alpn": ["resolve-alpn@1.2.1", "https://registry.npmmirror.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + + "responselike": ["responselike@2.0.1", "https://registry.npmmirror.com/responselike/-/responselike-2.0.1.tgz", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + + "retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "rimraf": ["rimraf@2.6.3", "https://registry.npmmirror.com/rimraf/-/rimraf-2.6.3.tgz", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + + "roarr": ["roarr@2.15.4", "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", { "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" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + + "rollup": ["rollup@4.60.2", "https://registry.npmmirror.com/rollup/-/rollup-4.60.2.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sanitize-filename": ["sanitize-filename@1.6.4", "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.4.tgz", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], + + "sax": ["sax@1.6.0", "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "semver-compare": ["semver-compare@1.0.0", "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + + "serialize-error": ["serialize-error@7.0.1", "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-update-notifier": ["simple-update-notifier@2.0.0", "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + + "slice-ansi": ["slice-ansi@3.0.0", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "smol-toml": ["smol-toml@1.6.1", "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.6.1.tgz", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "sonner": ["sonner@2.0.7", "https://registry.npmmirror.com/sonner/-/sonner-2.0.7.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + + "source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "sprintf-js": ["sprintf-js@1.1.3", "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + + "stat-mode": ["stat-mode@1.0.0", "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + + "string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "sumchecker": ["sumchecker@3.0.1", "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + + "supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tailwind-merge": ["tailwind-merge@3.5.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + + "tailwindcss": ["tailwindcss@4.2.4", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.4.tgz", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], + + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + + "tapable": ["tapable@2.3.3", "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tar": ["tar@7.5.13", "https://registry.npmmirror.com/tar/-/tar-7.5.13.tgz", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + + "temp": ["temp@0.9.4", "https://registry.npmmirror.com/temp/-/temp-0.9.4.tgz", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], + + "temp-file": ["temp-file@3.4.0", "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + + "tiny-async-pool": ["tiny-async-pool@1.3.0", "https://registry.npmmirror.com/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], + + "tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "tmp": ["tmp@0.2.5", "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + + "tmp-promise": ["tmp-promise@3.0.3", "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + + "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@0.13.1", "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + + "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@6.25.0", "https://registry.npmmirror.com/undici/-/undici-6.25.0.tgz", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], + + "undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "universalify": ["universalify@2.0.1", "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "utf8-byte-length": ["utf8-byte-length@1.0.5", "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + + "verror": ["verror@1.10.1", "https://registry.npmmirror.com/verror/-/verror-1.10.1.tgz", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + + "vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + + "which": ["which@5.0.0", "https://registry.npmmirror.com/which/-/which-5.0.0.tgz", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + + "xmlbuilder": ["xmlbuilder@15.1.1", "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "https://registry.npmmirror.com/yallist/-/yallist-5.0.0.tgz", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yauzl": ["yauzl@2.10.0", "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@electron/asar/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/get/fs-extra": ["fs-extra@8.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/universal/fs-extra": ["fs-extra@11.3.4", "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + + "@electron/universal/minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "https://registry.npmmirror.com/@electron/get/-/get-3.1.0.tgz", { "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" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], + + "app-builder-lib/ci-info": ["ci-info@4.3.1", "https://registry.npmmirror.com/ci-info/-/ci-info-4.3.1.tgz", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "app-builder-lib/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "clone-response/mimic-response": ["mimic-response@1.0.1", "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + + "cross-spawn/which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "dir-compare/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "filelist/minimatch": ["minimatch@5.1.9", "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "glob/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "global-agent/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-1.7.2.tgz", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + + "lru-cache/yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "node-abi/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "node-api-version/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "node-gyp/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "node-gyp/which": ["which@6.0.1", "https://registry.npmmirror.com/which/-/which-6.0.1.tgz", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "postject/commander": ["commander@9.5.0", "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + + "radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "simple-update-notifier/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "tiny-async-pool/semver": ["semver@5.7.2", "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "vite/esbuild": ["esbuild@0.27.7", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.0", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "node-gyp/which/isexe": ["isexe@4.0.0", "https://registry.npmmirror.com/isexe/-/isexe-4.0.0.tgz", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..edb23aa --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/renderer/src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/config_qq_adapter.py b/config_qq_adapter.py deleted file mode 100644 index c91a2c7..0000000 --- a/config_qq_adapter.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- -""" -QQ适配器配置脚本 -用于首次运行时配置QQ适配器相关设置 -""" -import sys -from pathlib import Path -import toml - -try: - from modules.MaiBot.src.common.logger import get_logger - logger = get_logger("qq_adapter_config") -except ImportError: - import logging as logger - logger.basicConfig(level=logger.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - logger = logger.getLogger("qq_adapter_config") - - -def get_config_path() -> Path: - """获取配置文件路径""" - script_dir = Path(__file__).parent - config_path = script_dir / "modules" / "MaiBot-Napcat-Adapter" / "config.toml" - return config_path - - -def read_config_with_comments(file_path: Path) -> tuple[dict, list[str]]: - """读取配置文件,保留注释 - - Returns: - tuple: (配置字典, 原始文件行列表) - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - lines = f.readlines() - - config = toml.load(file_path) - return config, lines - - except Exception as e: - logger.error(f"读取配置文件失败: {e}") - raise - - -def update_config_preserve_comments(file_path: Path, config: dict, original_lines: list[str]) -> bool: - """更新配置文件,保留注释 - - Args: - file_path: 配置文件路径 - config: 更新后的配置字典 - original_lines: 原始文件行列表 - - Returns: - bool: 是否成功 - """ - try: - new_lines = [] - in_section = None - - for line in original_lines: - stripped = line.strip() - - # 检测section - if stripped.startswith('[') and stripped.endswith(']'): - section_name = stripped[1:-1].strip() - in_section = section_name - new_lines.append(line) - continue - - # 保留注释和空行 - if stripped.startswith('#') or not stripped: - new_lines.append(line) - continue - - # 处理配置项 - if '=' in line and in_section: - key = line.split('=')[0].strip() - - # 更新特定的配置项 - if in_section == 'chat': - if key == 'group_list': - indent = len(line) - len(line.lstrip()) - group_list = config.get('chat', {}).get('group_list', []) - new_lines.append(' ' * indent + f'group_list = {group_list}\n') - continue - elif key == 'private_list': - indent = len(line) - len(line.lstrip()) - private_list = config.get('chat', {}).get('private_list', []) - new_lines.append(' ' * indent + f'private_list = {private_list}\n') - continue - - # 保留其他行 - new_lines.append(line) - - # 写入文件 - with open(file_path, 'w', encoding='utf-8') as f: - f.writelines(new_lines) - - logger.info("配置文件已更新,注释已保留") - return True - - except Exception as e: - logger.error(f"更新配置文件失败: {e}") - return False - - -def input_qq_list(prompt: str) -> list[int]: - """交互式输入QQ号列表 - - Args: - prompt: 提示信息 - - Returns: - list: QQ号列表 - """ - print(f"\n{prompt}") - print("请输入QQ号,多个号码用逗号或空格分隔") - print("直接按回车跳过此项配置") - print("-" * 50) - - user_input = input(">>> ").strip() - - if not user_input: - logger.info("用户跳过此项配置") - return [] - - # 支持逗号和空格分隔 - qq_list = [] - separators = [',', ',', ' ', '\t'] - - # 替换所有分隔符为空格 - for sep in separators: - user_input = user_input.replace(sep, ' ') - - # 分割并转换为整数 - parts = user_input.split() - for part in parts: - try: - qq_num = int(part.strip()) - if qq_num > 0: - qq_list.append(qq_num) - else: - print(f"警告: 忽略无效的QQ号 '{part}'") - except ValueError: - print(f"警告: 忽略无效的输入 '{part}'") - - if qq_list: - logger.info(f"已添加 {len(qq_list)} 个QQ号: {qq_list}") - print(f"✓ 已添加 {len(qq_list)} 个号码") - - return qq_list - - -def configure_qq_adapter() -> bool: - """配置QQ适配器 - - Returns: - bool: 配置是否成功 - """ - try: - logger.info("开始配置QQ适配器") - print("=" * 50) - print("QQ适配器配置向导") - print("=" * 50) - print("\n本向导将帮助您配置群聊和私聊白名单") - print("白名单模式: 只有在名单中的群组/用户可以与机器人聊天") - - # 获取配置文件路径 - config_path = get_config_path() - - if not config_path.exists(): - logger.error(f"配置文件不存在: {config_path}") - print(f"\n错误: 配置文件不存在") - return False - - # 读取配置文件 - config, original_lines = read_config_with_comments(config_path) - - # 确保chat section存在 - if 'chat' not in config: - config['chat'] = {} - - # 配置群聊白名单 - group_list = input_qq_list("【群聊白名单配置】") - config['chat']['group_list'] = group_list - - # 配置私聊白名单 - private_list = input_qq_list("【私聊白名单配置】") - config['chat']['private_list'] = private_list - - # 保存配置 - print("\n正在保存配置...") - if update_config_preserve_comments(config_path, config, original_lines): - print("✓ 配置已保存") - print(f"\n配置文件位置: {config_path}") - print(f"群聊白名单: {len(group_list)} 个群组") - print(f"私聊白名单: {len(private_list)} 个用户") - logger.info("QQ适配器配置完成") - return True - else: - print("✗ 保存配置失败") - return False - - except Exception as e: - logger.error(f"配置QQ适配器时发生错误: {e}") - print(f"\n错误: {e}") - return False - - -def main() -> None: - """主函数""" - try: - if configure_qq_adapter(): - logger.info("QQ适配器配置成功") - print("\nQQ适配器配置成功!") - else: - logger.error("QQ适配器配置失败") - print("\nQQ适配器配置失败,请检查日志") - sys.exit(1) - - except KeyboardInterrupt: - logger.info("用户中断配置过程") - print("\n配置已被用户中断") - sys.exit(1) - except Exception as e: - logger.error(f"配置过程中出现未知错误: {e}") - print(f"\n配置失败: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/dev/PTY b/dev/PTY new file mode 160000 index 0000000..09fc369 --- /dev/null +++ b/dev/PTY @@ -0,0 +1 @@ +Subproject commit 09fc369dfa278504831260de2771d7cbd98d01c4 diff --git a/dev/maimai-v2.jpg b/dev/maimai-v2.jpg new file mode 100644 index 0000000..bf1e44c Binary files /dev/null and b/dev/maimai-v2.jpg differ diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..59017c4 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,86 @@ +# Release Engineering + +本文档记录桌面版发布流程。当前目标平台是 Windows x64,安装器使用 `electron-builder` 的 NSIS target。 + +## 本地发布 + +1. 准备依赖: + + ```bash + bun install + ``` + +2. 准备 release payload。构建完整包时,仓库根目录必须存在: + + ```text + runtime/python/python.exe + runtime/python/DLLs/ + runtime/python/Lib/ + runtime/python/Scripts/pip.exe + runtime/git/bin/git.exe + modules/MaiBot/bot.py + modules/napcat/NapCatWinBootMain.exe + ``` + +3. 执行发布检查: + + ```bash + bun run release:check + ``` + +4. 构建 Windows x64 安装包: + + ```bash + bun run release:win + ``` + +安装包会输出到 `release/`,默认同时生成 `full` 完整包和 `lite` 精简包。`lite` 不包含 `runtime/python/` 与 `runtime/git/`,运行时会寻找系统 Python 3.12+ 与系统 Git,并在缺失时给出下载入口。 + +只构建精简包时,payload 可以省略 `runtime/python/` 与 `runtime/git/`: + +```bash +bun run release:win:lite +``` + +## GitHub Actions 发布 + +`Windows Release` 工作流是手动触发的。推荐上传一个 payload zip 到稳定位置,然后在工作流输入里填写: + +- `payload_url`:zip 下载地址。 +- `payload_sha256`:可选,填了会校验 zip 完整性。 +- `create_github_release`:是否创建 draft GitHub Release。 +- `tag_name`:创建 GitHub Release 时必填,例如 `v0.1.0`。 +- `prerelease`:是否标记为预发布。 + +payload zip 支持两种结构: + +```text +payload.zip + runtime/ + modules/ +``` + +或: + +```text +payload.zip + MaiBotOneKeyPayload/ + runtime/ + modules/ +``` + +## 保留的数据 + +安装器卸载时不会删除 Electron userData。应用 userData 按安装目录 hash 隔离,所以同一台机器复制两份安装目录时,可以分别运行两套实例与数据。 + +模块代码更新策略另行实现:后续强制覆盖模块代码时,需要保留配置和数据,并要求用户二次确认。 + +## Windows 实机冒烟清单 + +- 安装器可正常安装到默认目录和自定义目录。 +- 同一安装目录重复启动只保留一个实例。 +- 复制两份安装目录后可以分别启动。 +- MaiBot Core 和 NapCat 能被 Electron 启停。 +- 端口冲突时明确报错,不复用外部进程。 +- 关闭窗口时能选择最小化或全部退出。 +- 强杀服务后再次启动不会残留 PTY session。 diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..d3d0b3b --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,27 @@ +import { resolve } from "node:path"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + }, + preload: { + plugins: [externalizeDepsPlugin()], + }, + renderer: { + root: "src/renderer", + server: { + host: "127.0.0.1", + port: 5173, + }, + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": resolve("src/renderer/src"), + "@shared": resolve("src/shared"), + }, + }, + }, +}); diff --git a/emoji2.png b/emoji2.png new file mode 100644 index 0000000..620a704 Binary files /dev/null and b/emoji2.png differ diff --git a/init_napcat.py b/init_napcat.py deleted file mode 100644 index 1bd21dc..0000000 --- a/init_napcat.py +++ /dev/null @@ -1,192 +0,0 @@ -import re -import json -import tomlkit # 替换 tomli -from pathlib import Path - -def is_valid_qq(qq_str): - # 检查是否为纯数字 - return bool(re.match(r'^\d+$', qq_str)) - -def get_available_versions(): - """获取可用的QQ版本列表""" - versions = [] - - # 检查napcat目录中的版本 - napcat_versions_dir = Path('./modules/napcat/versions') - if napcat_versions_dir.exists(): - versions.extend([ - version_dir.name for version_dir in napcat_versions_dir.iterdir() - if version_dir.is_dir() and version_dir.name != 'config.json' - ]) - - # 检查napcatframework目录中的版本(合并去重) - napcatframework_versions_dir = Path('./modules/napcatframework/versions') - if napcatframework_versions_dir.exists(): - framework_versions = [ - version_dir.name for version_dir in napcatframework_versions_dir.iterdir() - if version_dir.is_dir() and version_dir.name != 'config.json' and version_dir.name not in versions - ] - versions.extend(framework_versions) - - return sorted(versions) - -def create_napcat_config(qq_number): - # 创建napcat配置文件 - config = { - "fileLog": False, - "consoleLog": True, - "fileLogLevel": "debug", - "consoleLogLevel": "info", - "packetBackend": "auto", - "packetServer": "", - "o3HookMode": 1 - } - - # 获取所有可用版本 - available_versions = get_available_versions() - - if not available_versions: - print("警告:未找到任何QQ版本,使用默认版本") - available_versions = ["9.9.21-39038"] - - print(f"找到 {len(available_versions)} 个QQ版本:{', '.join(available_versions)}") - - # 为每个版本创建配置文件 - for version in available_versions: - # napcat路径 - config_dir_1 = Path(f'./modules/napcat/versions/{version}/resources/app/napcat/config') - config_dir_1.mkdir(parents=True, exist_ok=True) - - config_path_1 = config_dir_1 / f'napcat_{qq_number}.json' - with open(config_path_1, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=2, ensure_ascii=False) - print(f"已创建napcat配置文件:{config_path_1}") - - # napcatframework路径 - config_dir_2 = Path(f'./modules/napcatframework/versions/{version}/resources/app/LiteLoader/plugins/NapCat/config') - config_dir_2.mkdir(parents=True, exist_ok=True) - - config_path_2 = config_dir_2 / f'napcat_{qq_number}.json' - with open(config_path_2, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=2, ensure_ascii=False) - print(f"已创建napcatframework配置文件:{config_path_2}") - -def create_onebot_config(qq_number): - # 创建OneBot11配置文件 - config = { - "network": { - "httpServers": [], - "httpSseServers": [], - "httpClients": [], - "websocketServers": [], - "websocketClients": [ - { - "enable": True, - "name": "MaiBot Main", - "url": "ws://localhost:8095", - "reportSelfMessage": False, - "messagePostFormat": "array", - "token": "", - "debug": False, - "heartInterval": 30000, - "reconnectInterval": 30000 - } - ], - "plugins": [] - }, - "musicSignUrl": "", - "enableLocalFile2Url": False, - "parseMultMsg": False - } - - # 获取所有可用版本 - available_versions = get_available_versions() - - if not available_versions: - print("警告:未找到任何QQ版本,使用默认版本") - available_versions = ["9.9.21-39038"] - - print(f"为 {len(available_versions)} 个版本创建OneBot11配置") - - # 为每个版本创建配置文件 - for version in available_versions: - # napcat路径 - config_dir_1 = Path(f'./modules/napcat/versions/{version}/resources/app/napcat/config') - config_dir_1.mkdir(parents=True, exist_ok=True) - - config_path_1 = config_dir_1 / f'onebot11_{qq_number}.json' - with open(config_path_1, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=2, ensure_ascii=False) - print(f"已创建OneBot11配置文件:{config_path_1}") - - # napcatframework路径 - config_dir_2 = Path(f'./modules/napcatframework/versions/{version}/resources/app/LiteLoader/plugins/NapCat/config') - config_dir_2.mkdir(parents=True, exist_ok=True) - - config_path_2 = config_dir_2 / f'onebot11_{qq_number}.json' - with open(config_path_2, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=2, ensure_ascii=False) - print(f"已创建OneBot11配置文件:{config_path_2}") - -def update_qq_in_config(path: str, qq_number: int): # 确保 qq_number 是整数 - config_path = Path(path) - - # 如果配置文件不存在,尝试从模板创建 - if not config_path.exists() and 'config' in str(config_path): - template_path = config_path.parent.parent / 'template' / config_path.name.replace('bot_config.toml', 'bot_config_template.toml') - if template_path.exists(): - # 确保配置目录存在 - config_path.parent.mkdir(parents=True, exist_ok=True) - # 从模板复制配置文件 - import shutil - shutil.copy2(template_path, config_path) - print(f"已从模板创建配置文件: {config_path}") - - try: - # 读取原始文件内容 - with open(config_path, 'r', encoding='utf-8') as f: - content = f.read() - - # 解析 TOML 内容 - doc = tomlkit.parse(content) - - # 更新 qq 值 - if 'bot' not in doc: - doc['bot'] = tomlkit.table() # 如果 bot 表不存在则创建 - doc['bot']['qq_account'] = qq_number # qq_number 已经是整数 - - # 写入更新后的内容 - with open(config_path, 'w', encoding='utf-8') as f: - tomlkit.dump(doc, f) - - except FileNotFoundError: - print(f"错误:配置文件 {config_path} 未找到。") - raise - except tomlkit.exceptions.TOMLKitError as e: - print(f"错误:解析配置文件 {config_path} 失败:{e}") - raise - except Exception as e: - print(f"错误:更新配置文件 {config_path} 时发生未知错误:{e}") - raise - -def main(): - while True: - qq_input = input('请输入QQ号:') - if not is_valid_qq(qq_input): - print('错误:请输入有效的QQ号(纯数字)') - continue - - qq_number_int = int(qq_input) # 转换为整数 - try: - update_qq_in_config('./modules/MaiBot/config/bot_config.toml', qq_number_int) - update_qq_in_config('./modules/MaiBot/template/bot_config_template.toml', qq_number_int) - create_onebot_config(qq_input) # create_onebot_config 和 create_napcat_config 需要字符串类型的 qq - create_napcat_config(qq_input) - print(f'成功更新QQ号为:{qq_input}并创建所有必要的配置文件') - break - except Exception as e: - print(f'更新配置文件时出错:{str(e)}') - continue - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/mai.png b/mai.png new file mode 100644 index 0000000..efa89f4 Binary files /dev/null and b/mai.png differ diff --git a/mai2.png b/mai2.png new file mode 100644 index 0000000..70cb26b Binary files /dev/null and b/mai2.png differ diff --git a/main.py b/main.py deleted file mode 100644 index c564afb..0000000 --- a/main.py +++ /dev/null @@ -1,343 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import re -import sys -import subprocess -import shutil -try: - from modules.MaiBot.src.common.logger import get_logger - logger = get_logger("init") -except ImportError: - import logging as logger - logger.basicConfig(level=logger.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - logger = logger.getLogger("init") - -from pathlib import Path -from typing import Optional - -def get_absolute_path(relative_path: str) -> str: - """获取绝对路径 - - Args: - relative_path: 相对路径 - - Returns: - str: 绝对路径 - """ - script_dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.join(script_dir, relative_path) -def check_and_create_config_files() -> bool: - """检测并创建所有必要的配置文件 - - Returns: - bool: 所有配置文件检测和创建是否成功 - """ - config_checks = [ - { - 'name': 'MaiBot配置目录', - 'path': get_absolute_path('modules/MaiBot/config'), - 'is_directory': True - }, - { - 'name': 'MaiBot主配置文件', - 'path': get_absolute_path('modules/MaiBot/config/bot_config.toml'), - 'template': get_absolute_path('modules/MaiBot/template/bot_config_template.toml'), - 'is_directory': False - }, - { - 'name': 'MaiBot-模型配置文件', - 'path': get_absolute_path('modules/MaiBot/config/model_config.toml'), - 'template': get_absolute_path('modules/MaiBot/template/model_config_template.toml'), - 'is_directory': False - }, - { - 'name': 'MaiBot环境文件', - 'path': get_absolute_path('modules/MaiBot/.env'), - 'template': get_absolute_path('modules/MaiBot/template/template.env'), - 'is_directory': False - }, - { - 'name': 'NapCat适配器配置文件', - 'path': get_absolute_path('modules/MaiBot-Napcat-Adapter/config.toml'), - 'template': get_absolute_path('modules/MaiBot-Napcat-Adapter/template/template_config.toml'), - 'is_directory': False - } - ] - - all_success = True - - for config in config_checks: - try: - if config['is_directory']: - # 检测目录 - if not os.path.exists(config['path']): - os.makedirs(config['path'], exist_ok=True) - logger.info(f"已创建目录: {config['name']}") - else: - logger.info(f"目录已存在: {config['name']}") - else: - # 检测配置文件 - if not os.path.exists(config['path']): - if 'template' in config and os.path.exists(config['template']): - # 确保目标目录存在 - target_dir = os.path.dirname(config['path']) - if not os.path.exists(target_dir): - os.makedirs(target_dir, exist_ok=True) - - # 复制模板文件 - shutil.copy2(config['template'], config['path']) - logger.info(f"已从模板创建配置文件: {config['name']}") - else: - logger.warning(f"模板文件不存在,无法创建: {config['name']}") - logger.warning(f"模板路径: {config.get('template', '未指定')}") - all_success = False - else: - logger.info(f"配置文件已存在: {config['name']}") - - except Exception as e: - logger.error(f"处理配置文件时出错 {config['name']}: {str(e)}") - all_success = False - - if all_success: - logger.info("所有配置文件检测完成!") - else: - logger.warning("部分配置文件处理失败,请检查上述错误信息") - - return all_success -# 配置日志 - -def get_python_interpreter() -> Optional[Path]: - """获取Python解释器路径""" - try: - # 尝试多个可能的路径 - possible_paths = [ - Path(__file__).parent / "runtime" / "python31211" / "bin" / "python.exe", - Path(__file__).parent / "runtime" / "python31211" / "python.exe", - Path(sys.executable), # 当前Python解释器 - ] - - for python_path in possible_paths: - if python_path.exists() and python_path.is_file(): - logger.info(f"找到Python解释器: {python_path}") - return python_path - - logger.error("未找到可用的Python解释器") - return None - - except Exception as e: - logger.error(f"获取Python解释器路径时出错: {e}") - return None - -def is_first_run() -> bool: - """检查是否是首次运行 - - 通过检查 runtime/.initialized 或 runtime/.gitkeep 文件是否存在来判断 - """ - runtime_dir = Path(__file__).parent / "runtime" - new_marker = runtime_dir / ".initialized" - legacy_marker = runtime_dir / ".gitkeep" # 兼容旧版本 - - # 如果任一标记文件存在,则不是首次运行 - if new_marker.exists() or legacy_marker.exists(): - logger.info("检测到非首次运行 (标记文件存在)") - return False - - logger.info("首次运行检测: 未找到初始化标记文件") - return True - -def run_python_script(script_name: str) -> bool: - """运行同一目录下的Python脚本""" - try: - # 获取当前脚本目录 - current_dir = Path(__file__).parent - - # 构建目标脚本路径 - target_script = current_dir / script_name - - # 检查目标脚本是否存在 - if not target_script.exists(): - logger.error(f"目标脚本不存在: {target_script}") - return False - - # 获取Python解释器路径 - python_path = get_python_interpreter() - if python_path is None: - logger.error("无法找到Python解释器") - return False - - logger.info(f"开始执行脚本: {script_name}") - - # 执行目标脚本 - result = subprocess.run( - [str(python_path), str(target_script)], - capture_output=False, # 保持输出到控制台 - text=True, - timeout=30000, # 5分钟超时 - cwd=str(current_dir) # 设置工作目录 - ) - - if result.returncode == 0: - logger.info(f"脚本执行成功: {script_name}") - return True - else: - logger.error(f"脚本执行失败: {script_name}, 返回码: {result.returncode}") - return False - - except subprocess.TimeoutExpired: - logger.error(f"脚本执行超时: {script_name}") - return False - except FileNotFoundError as e: - logger.error(f"文件未找到: {e}") - return False - except Exception as e: - logger.error(f"执行脚本时出错: {script_name}, 错误: {e}") - return False - -def safe_system_command(command: str, timeout: int = 30) -> bool: - """安全地执行系统命令 - - Args: - command: 要执行的命令 - timeout: 超时时间(秒) - - Returns: - bool: 命令执行是否成功 - """ - try: - logger.info(f"执行系统命令: {command}") - result = subprocess.run( - command, - shell=True, - timeout=timeout, - capture_output=False, - text=True - ) - - if result.returncode == 0: - logger.info(f"系统命令执行成功: {command}") - return True - else: - logger.warning(f"系统命令执行失败: {command}, 返回码: {result.returncode}") - return False - - except subprocess.TimeoutExpired: - logger.error(f"系统命令执行超时: {command}") - return False - except Exception as e: - logger.error(f"执行系统命令时出错: {command}, 错误: {e}") - return False - -def setup_webui_dependencies() -> bool: - """(弃用) 保留占位以兼容旧代码调用,直接返回 True""" - logger.info("setup_webui_dependencies 已弃用,直接跳过。") - return True - -def check_dir_legal() -> bool: - """检查当前目录是否包含中文等特殊字符 - - Returns: - bool: True表示目录包含非法字符,False表示目录合法 - """ - try: - # 获取当前工作目录 - current_path = os.getcwd() - - # 检查路径是否包含中文字符(Unicode范围) - has_chinese = bool(re.search(r'[\u3000-\u303f\u4e00-\u9fff\uff00-\uffef]', current_path)) - - if has_chinese: - error_msg = f"警告:当前路径包含中文等特殊字符: {current_path}" - print(error_msg) - print("禁止启动,已自动退出,请将一键包移动到非中文目录再启动!") - logger.error(error_msg) - logger.error("程序因路径包含特殊字符而退出") - return True - else: - logger.info(f"路径检查通过: {current_path}") - return False - - except Exception as e: - error_msg = f"检查目录路径时出错: {e}" - print(error_msg) - logger.error(error_msg) - # 出错时为安全起见,认为路径不合法 - return True - -def main() -> None: - """主函数""" - try: - logger.info("MaiBot 一键包启动") - check_and_create_config_files() - - # 检查目录路径合法性 - if check_dir_legal(): - logger.error("目录路径不合法,程序退出") - sys.exit(1) - - # 检查是否首次运行 - if is_first_run(): - # 初始化一键包 - logger.info("首次运行一键包,执行初始化操作") - print("首次运行一键包,执行初始化操作……") - - if not run_python_script("update_modules.py"): - logger.error("模块更新失败") - return - - # (已移除) 旧版 WebUI 依赖安装步骤已废弃 - - print("======================") - print("正在执行NapCat初始化脚本...") - print("======================") - - if not run_python_script("init_napcat.py"): - logger.error("NapCat初始化失败") - return - - print("======================") - print("正在配置QQ适配器...") - print("======================") - - if not run_python_script("config_qq_adapter.py"): - logger.error("QQ适配器配置失败") - return - - # 所有初始化步骤完成,创建标记文件 - try: - runtime_dir = Path(__file__).parent / "runtime" - init_marker = runtime_dir / ".initialized" - runtime_dir.mkdir(parents=True, exist_ok=True) - init_marker.write_text('initialized', encoding='utf-8') - logger.info("初始化完成,已创建标记文件: .initialized") - except Exception as e: - logger.warning(f"创建初始化标记文件失败: {e}") - - print("3秒后启动MaiBot Client...") - safe_system_command("timeout /t 3 /nobreak > nul") - safe_system_command("cls") - - # 首次初始化后直接进入 start.py(主菜单/主逻辑) - if not run_python_script("start.py"): - logger.error("首次运行后启动主程序失败") - return - else: - # 非首次运行 - logger.info("检测到不是首次运行,正在跳过向导启动 MaiBot Core") - print("检测到不是首次运行,正在跳过向导启动 MaiBot Core...") - - if not run_python_script("start.py"): - logger.error("启动主程序失败") - return - - logger.info("程序执行完成") - - except KeyboardInterrupt: - logger.info("用户中断程序执行") - print("\n程序已被用户中断") - except Exception as e: - logger.error(f"程序执行过程中出现未知错误: {e}") - print(f"程序执行失败: {e}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f2ef1b5 --- /dev/null +++ b/package.json @@ -0,0 +1,150 @@ +{ + "name": "maibot-onekey-desktop", + "version": "0.3.3", + "description": "Electron desktop shell for MaiBot OneKey.", + "author": "MotricSeven", + "license": "GPL-3.0-only", + "type": "module", + "main": "out/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "dev:web": "vite --config vite.renderer.config.ts", + "typecheck": "tsc --noEmit -p tsconfig.json", + "build": "tsc --noEmit -p tsconfig.json && electron-vite build", + "check": "bun run typecheck && bun run build", + "preview": "electron-vite preview", + "release:check": "bun run scripts/release/check-windows-payload.ts", + "release:patch-nsis": "bun run scripts/release/patch-electron-builder-nsis.ts", + "release:win": "bun run release:check && bun run release:patch-nsis && bun run build && bun run scripts/release/build-windows-variants.ts", + "pack:win": "bun run release:win" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tanstack/react-virtual": "^3.13.24", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "iconv-lite": "^0.7.2", + "lucide-react": "^0.468.0", + "next-themes": "^0.4.6", + "node-pty": "^1.1.0", + "radix-ui": "^1.4.3", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "smol-toml": "^1.6.1", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "ws": "^8.20.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.13", + "@types/bun": "^1.2.23", + "@types/node": "^22.15.34", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@types/ws": "^8.18.1", + "@vitejs/plugin-react": "^5.0.3", + "electron": "^38.1.2", + "electron-builder": "^26.0.12", + "electron-vite": "^4.0.1", + "tailwindcss": "^4.1.13", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.9.3", + "vite": "^7.1.7" + }, + "build": { + "appId": "org.maibot.onekey.desktop", + "productName": "MaiBot OneKey", + "npmRebuild": false, + "buildDependenciesFromSource": false, + "compression": "store", + "directories": { + "output": "release", + "buildResources": "resources" + }, + "asarUnpack": [ + "node_modules/node-pty/**" + ], + "files": [ + "out/**", + "package.json", + "node_modules/node-pty/**", + "node_modules/node-addon-api/**" + ], + "extraResources": [ + { + "from": "resources/icon.png", + "to": "icon.png" + }, + { + "from": "runtime", + "to": "runtime", + "filter": [ + "**/*", + "!python/Doc/**", + "!**/.git/**", + "!**/__pycache__/**", + "!**/*.pyc" + ] + }, + { + "from": "release-assets/python-overrides", + "to": "python-overrides", + "filter": [ + "**/*", + "!**/__pycache__/**", + "!**/*.pyc" + ] + }, + { + "from": "modules", + "to": "modules", + "filter": [ + "**/*", + "!**/__pycache__/**", + "!**/*.pyc", + "!**/.git/index.lock", + "!**/.git/objects/pack/.tmp-*", + "!MaiBot/config/**", + "!MaiBot/data/**", + "!MaiBot/maibot_statistics.html", + "!MaiBot/plugins/sengokucola_better-image/**", + "!MaiBot/plugins/__init__.py", + "!MaiBot/plugins/napcat-adapter/.git/**", + "!MaiBot/plugins/snowluma-adapter/.git/**", + "!napcat/napcat/config/webui.json", + "!napcat/**/*linux*", + "!napcat/**/*darwin*", + "!napcat/**/*arm64*", + "!napcat/versions/**/resources/app/napcat/config/webui.json", + "!napcatframework/versions/**/resources/app/LiteLoader/plugins/NapCat/config/webui.json", + "!napcat/.git/**" + ] + } + ], + "win": { + "target": "nsis", + "icon": "resources/icon.ico", + "requestedExecutionLevel": "asInvoker", + "artifactName": "${productName}-${version}-win-${arch}.${ext}" + }, + "mac": { + "icon": "resources/icon.icns" + }, + "linux": { + "icon": "resources/icon.png" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "include": "installer.nsh", + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "deleteAppDataOnUninstall": false, + "shortcutName": "MaiBot OneKey" + } + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 33a644c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,21 +0,0 @@ -loguru -tomlkit -toml -attrs -structlog -pillow -aiofiles -fastapi -uvicorn -pydantic -python-multipart -python-json-logger -aiofiles -websockets -pydantic-settings -toml -tomlkit -psutil -argon2-cffi -requests -aiofiles diff --git a/resources/.gitkeep b/resources/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/resources/.gitkeep @@ -0,0 +1 @@ + diff --git a/resources/icon.icns b/resources/icon.icns new file mode 100644 index 0000000..4d75fab Binary files /dev/null and b/resources/icon.icns differ diff --git a/resources/icon.ico b/resources/icon.ico new file mode 100644 index 0000000..796f5d1 Binary files /dev/null and b/resources/icon.ico differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..3c0e9a7 Binary files /dev/null and b/resources/icon.png differ diff --git a/resources/installer.nsh b/resources/installer.nsh new file mode 100644 index 0000000..5abb1ff --- /dev/null +++ b/resources/installer.nsh @@ -0,0 +1,12 @@ +!ifndef BUILD_UNINSTALLER +!macro customPageAfterChangeDir + ShowInstDetails hide +!macroend +!endif + +!macro customHeader + ShowInstDetails hide + !ifdef BUILD_UNINSTALLER + ShowUninstDetails hide + !endif +!macroend diff --git a/scripts/release/build-windows-variants.ts b/scripts/release/build-windows-variants.ts new file mode 100644 index 0000000..9f1d303 --- /dev/null +++ b/scripts/release/build-windows-variants.ts @@ -0,0 +1,106 @@ +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { copyFile, mkdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import process from "node:process"; + +const root = process.cwd(); + +function hasEmbeddedGit(): boolean { + return [ + join(root, "runtime", "git", "bin", "git.exe"), + join(root, "runtime", "git", "cmd", "git.exe"), + join(root, "runtime", "git", "git.exe"), + join(root, "runtime", "git", "bin", "git"), + ].some((path) => existsSync(path)); +} + +function hasEmbeddedPython(): boolean { + return [ + join(root, "runtime", "python", "python.exe"), + join(root, "runtime", "python", "bin", "python.exe"), + join(root, "runtime", "python", "python"), + join(root, "runtime", "python", "bin", "python3"), + ].some((path) => existsSync(path)); +} + +type Variant = "basic" | "full"; +type ArtifactVariant = "lite" | "full"; + +function run(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: root, + stdio: "inherit", + shell: false, + }); + + child.on("error", reject); + child.on("exit", (code) => { + if (code === 0) { + resolve(); + return; + } + + reject(new Error(`${command} exited with code ${code ?? "unknown"}`)); + }); + }); +} + +async function main(): Promise { + if (!hasEmbeddedGit()) { + throw new Error("Cannot build the standard installer because runtime/git is missing."); + } + if (!hasEmbeddedPython()) { + throw new Error("Cannot build the standard installer because runtime/python is missing."); + } + + for (const variant of ["basic", "full"] as Variant[]) { + await buildVariant(variant); + } +} + +async function buildVariant(variant: Variant): Promise { + console.log(`[release] Preparing ${variant} Python overlay`); + await run(process.execPath, [ + join(root, "scripts", "release", "prepare-python-overrides.ts"), + variant, + ]); + + console.log(`[release] Building Windows x64 ${variant} installer`); + const artifactVariant = variant === "basic" ? "lite" : "full"; + await run(process.execPath, [ + join(root, "node_modules", "electron-builder", "cli.js"), + "--win", + "nsis", + "--x64", + `--config.win.artifactName=MaiBot OK-\${version}-win-${artifactVariant}.\${ext}`, + ]); + + if (variant === "basic") { + await copyLatestMetadata("lite"); + } else { + await copyLatestMetadata("full"); + } +} + +async function copyLatestMetadata(variant: ArtifactVariant): Promise { + const releaseRoot = join(root, "release"); + await mkdir(releaseRoot, { recursive: true }); + const source = join(releaseRoot, "latest.yml"); + if (existsSync(source)) { + await copyFile(source, join(releaseRoot, `latest-${variant}.yml`)); + } +} + +async function cleanupPythonOverrides(): Promise { + await rm(join(root, "release-assets", "python-overrides"), { recursive: true, force: true }); + await mkdir(join(root, "release-assets", "python-overrides"), { recursive: true }); + await writeFile(join(root, "release-assets", "python-overrides", ".keep"), "", "utf8"); +} + +try { + await main(); +} finally { + await cleanupPythonOverrides(); +} diff --git a/scripts/release/check-windows-payload.ts b/scripts/release/check-windows-payload.ts new file mode 100644 index 0000000..9091aa3 --- /dev/null +++ b/scripts/release/check-windows-payload.ts @@ -0,0 +1,356 @@ +import { readdir, stat } from "node:fs/promises"; +import { join, relative } from "node:path"; +import process from "node:process"; + +type PathKind = "file" | "dir"; + +type Candidate = { + path: string; + kind: PathKind; + contains?: string[]; +}; + +type Requirement = { + label: string; + required: boolean; + candidates: Candidate[]; +}; + +const root = process.cwd(); +const pythonBootstrapPackages = new Set([ + "pip", + "pip.dist-info", + "setuptools", + "setuptools.dist-info", + "wheel", + "wheel.dist-info", + "pkg_resources", + "_distutils_hack", + "distutils-precedence.pth", +]); +const pythonBootstrapScripts = new Set([ + "pip.exe", + "pip3.exe", + "pip3.12.exe", + "__pycache__", +]); + +function file(path: string): Candidate { + return { path: join(root, path), kind: "file" }; +} + +function dir(path: string): Candidate { + return { path: join(root, path), kind: "dir" }; +} + +function dirContaining(path: string, contains: string[]): Candidate { + return { path: join(root, path), kind: "dir", contains }; +} + +const requirements: Requirement[] = [ + { + label: "runtime directory", + required: true, + candidates: [dir("runtime")], + }, + { + label: "portable Python directory", + required: true, + candidates: [dir("runtime/python")], + }, + { + label: "portable Python executable", + required: true, + candidates: [file("runtime/python/python.exe"), file("runtime/python/bin/python.exe")], + }, + { + label: "portable Python standard library", + required: true, + candidates: [dir("runtime/python/Lib"), dir("runtime/python/lib")], + }, + { + label: "portable Python extension modules", + required: true, + candidates: [dir("runtime/python/DLLs")], + }, + { + label: "portable Python pip command", + required: true, + candidates: [file("runtime/python/Scripts/pip.exe"), file("runtime/python/bin/pip")], + }, + { + label: "portable Python pip package", + required: true, + candidates: [dir("runtime/python/Lib/site-packages/pip"), dir("runtime/python/lib/site-packages/pip")], + }, + { + label: "embedded Git directory", + required: true, + candidates: [dir("runtime/git")], + }, + { + label: "embedded Git executable", + required: true, + candidates: [file("runtime/git/bin/git.exe"), file("runtime/git/cmd/git.exe"), file("runtime/git/git.exe")], + }, + { + label: "modules directory", + required: true, + candidates: [dir("modules")], + }, + { + label: "MaiBot module", + required: true, + candidates: [dir("modules/MaiBot")], + }, + { + label: "MaiBot entry", + required: true, + candidates: [file("modules/MaiBot/bot.py")], + }, + { + label: "MaiBot napcat-adapter plugin", + required: true, + candidates: [dir("modules/MaiBot/plugins/napcat-adapter")], + }, + { + label: "MaiBot snowluma-adapter plugin", + required: true, + candidates: [dir("modules/MaiBot/plugins/snowluma-adapter")], + }, + { + label: "MaiBot snowluma-adapter entry", + required: true, + candidates: [file("modules/MaiBot/plugins/snowluma-adapter/plugin.py")], + }, + { + label: "NapCat module", + required: true, + candidates: [dir("modules/napcat")], + }, + { + label: "NapCat Windows runtime", + required: true, + candidates: [file("modules/napcat/node.exe"), file("modules/napcat/NapCatWinBootMain.exe")], + }, + { + label: "SnowLuma module", + required: true, + candidates: [dir("modules/SnowLuma")], + }, + { + label: "SnowLuma entry", + required: true, + candidates: [file("modules/SnowLuma/index.mjs")], + }, + { + label: "SnowLuma Windows runtime", + required: true, + candidates: [file("modules/SnowLuma/node.exe")], + }, + { + label: "SnowLuma native binding", + required: true, + candidates: [ + file("modules/SnowLuma/native/snowluma-win32-x64.node"), + file("modules/SnowLuma/native/snowluma-win32-x64.dll"), + ], + }, + { + label: "node-pty Windows pty binding", + required: true, + candidates: [file("node_modules/node-pty/prebuilds/win32-x64/pty.node")], + }, + { + label: "node-pty Windows conpty binding", + required: true, + candidates: [file("node_modules/node-pty/prebuilds/win32-x64/conpty.node")], + }, + { + label: "node-pty Windows console-list binding", + required: true, + candidates: [file("node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node")], + }, + { + label: "NapCat version resources", + required: true, + candidates: [ + file("modules/napcat/napcat/package.json"), + dirContaining("modules/napcat/versions", [join("resources", "app", "package.json")]), + dirContaining("modules/napcatframework/versions", [join("resources", "app", "package.json")]), + ], + }, + { + label: "Windows app icon", + required: false, + candidates: [file("resources/icon.ico")], + }, +]; + +async function matches(candidate: Candidate): Promise { + try { + const info = await stat(candidate.path); + if (candidate.kind === "file") { + return info.isFile(); + } + if (!info.isDirectory()) { + return false; + } + if (!candidate.contains?.length) { + return true; + } + return directoryHasAny(candidate.path, candidate.contains); + } catch { + return false; + } +} + +async function directoryHasAny(directory: string, relativePaths: string[]): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + for (const relativePath of relativePaths) { + try { + const info = await stat(join(directory, entry.name, relativePath)); + if (info.isFile()) { + return true; + } + } catch { + // Try the next known NapCat layout. + } + } + } + return false; +} + +async function pathExists(path: string): Promise { + try { + await stat(path); + return true; + } catch { + return false; + } +} + +function normalizePythonPackageEntry(name: string): string { + return name + .replace(/-\d.+(?:\.dist-info|\.egg-info)$/iu, ".dist-info") + .replace(/[-_]+/gu, "-") + .toLowerCase(); +} + +async function findPythonSitePackages(): Promise { + const candidates = [ + join(root, "runtime", "python", "Lib", "site-packages"), + join(root, "runtime", "python", "lib", "site-packages"), + ]; + + for (const candidate of candidates) { + if (await pathExists(candidate)) { + return candidate; + } + } + + return undefined; +} + +async function checkLeanPythonRuntime(): Promise { + const sitePackages = await findPythonSitePackages(); + if (!sitePackages) { + return []; + } + + const entries = await readdir(sitePackages, { withFileTypes: true }); + return entries + .map((entry) => entry.name) + .filter((name) => name !== "__pycache__") + .filter((name) => !pythonBootstrapPackages.has(normalizePythonPackageEntry(name))) + .sort((left, right) => left.localeCompare(right, "en-US", { numeric: true, sensitivity: "base" })); +} + +async function checkLeanPythonScripts(): Promise { + const scriptsPath = join(root, "runtime", "python", "Scripts"); + if (!(await pathExists(scriptsPath))) { + return []; + } + + const entries = await readdir(scriptsPath, { withFileTypes: true }); + return entries + .map((entry) => entry.name) + .filter((name) => !pythonBootstrapScripts.has(name)) + .sort((left, right) => left.localeCompare(right, "en-US", { numeric: true, sensitivity: "base" })); +} + +function describeCandidates(candidates: Candidate[]): string { + return candidates.map((candidate) => relative(root, candidate.path)).join(" or "); +} + +async function main(): Promise { + const failures: Requirement[] = []; + + console.log("Checking Windows release payload..."); + + for (const requirement of requirements) { + const matched = []; + for (const candidate of requirement.candidates) { + if (await matches(candidate)) { + matched.push(candidate); + } + } + + if (matched.length > 0) { + console.log(`[ok] ${requirement.label}: ${relative(root, matched[0].path)}`); + continue; + } + + const prefix = requirement.required ? "[missing]" : "[warn]"; + console.log(`${prefix} ${requirement.label}: ${describeCandidates(requirement.candidates)}`); + + if (requirement.required) { + failures.push(requirement); + } + } + + if (failures.length > 0) { + console.log(""); + console.log(`Release payload is incomplete (${failures.length} required item(s) missing).`); + console.log("Put runtime/ and modules/ in the repository root before running bun run release:win."); + process.exitCode = 1; + return; + } + + const bundledPythonPackages = await checkLeanPythonRuntime(); + const bundledPythonScripts = await checkLeanPythonScripts(); + if (bundledPythonPackages.length > 0 || bundledPythonScripts.length > 0) { + console.log(""); + console.log("[missing] portable Python should not contain application dependencies."); + console.log("Keep runtime/python lean: only Python itself plus pip/setuptools/wheel are allowed."); + console.log("Install MaiBot/dashboard dependencies into python-overrides at first run instead."); + if (bundledPythonPackages.length > 0) { + console.log(`Unexpected site-packages entries (${bundledPythonPackages.length}):`); + for (const name of bundledPythonPackages.slice(0, 30)) { + console.log(` - ${name}`); + } + if (bundledPythonPackages.length > 30) { + console.log(` ... and ${bundledPythonPackages.length - 30} more`); + } + } + if (bundledPythonScripts.length > 0) { + console.log(`Unexpected Scripts entries (${bundledPythonScripts.length}):`); + for (const name of bundledPythonScripts.slice(0, 30)) { + console.log(` - ${name}`); + } + if (bundledPythonScripts.length > 30) { + console.log(` ... and ${bundledPythonScripts.length - 30} more`); + } + } + process.exitCode = 1; + return; + } + + console.log("Windows standard release payload looks complete."); +} + +await main(); diff --git a/scripts/release/patch-electron-builder-nsis.ts b/scripts/release/patch-electron-builder-nsis.ts new file mode 100644 index 0000000..0d5cc91 --- /dev/null +++ b/scripts/release/patch-electron-builder-nsis.ts @@ -0,0 +1,257 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +async function patchFile( + path: string, + patchName: string, + pattern: RegExp, + replacement: string, + alreadyPatched: string | string[], + options: { optional?: boolean } = {}, +): Promise { + const content = await readFile(path, "utf8"); + const alreadyPatchedMarkers = Array.isArray(alreadyPatched) ? alreadyPatched : [alreadyPatched]; + if (alreadyPatchedMarkers.some((marker) => content.includes(marker))) { + console.log(`[ok] ${patchName}: already patched`); + return; + } + + if (!pattern.test(content)) { + if (options.optional) { + console.log(`[ok] ${patchName}: not needed`); + return; + } + throw new Error(`${patchName}: expected template content was not found in ${path}`); + } + + await writeFile(path, content.replace(pattern, replacement), "utf8"); + console.log(`[ok] ${patchName}: patched`); +} + +const nsisTemplateRoot = join(process.cwd(), "node_modules", "app-builder-lib", "templates", "nsis"); + +await patchFile( + join(nsisTemplateRoot, "installSection.nsh"), + "enable NSIS details output", + /(\$\{IfNot\} \$\{Silent\}\r?\n)\s*SetDetailsPrint none(\r?\n\$\{endif\})/u, + "$1 SetDetailsPrint both$2", + "SetDetailsPrint both", +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "show copied files in NSIS details", + /CopyFiles \/SILENT "\$PLUGINSDIR\\7z-out\\\*" \$OUTDIR/u, + 'CopyFiles "$PLUGINSDIR\\7z-out\\*" $OUTDIR', + [ + 'CopyFiles "$PLUGINSDIR\\7z-out\\*" $OUTDIR', + 'CopyFiles "$R2\\*" $OUTDIR', + 'Rename "$R2" "$OUTDIR"', + 'Rename "$R2" "$R0"', + ], +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "stage app extraction beside install dir before rename", + /Push \$OUTDIR\r?\n CreateDirectory "\$PLUGINSDIR\\7z-out"\r?\n ClearErrors\r?\n SetOutPath "\$PLUGINSDIR\\7z-out"([\s\S]*?)CopyFiles (?:\/SILENT )?"\$PLUGINSDIR\\7z-out\\\*" \$OUTDIR([\s\S]*?)RMDir \/r "\$PLUGINSDIR\\7z-out"([\s\S]*?)DoneExtract7za:\r?\n!macroend/u, + `Push $OUTDIR + StrCpy $R2 "$OUTDIR.__installing-$packageArch" + StrCpy $R3 "$OUTDIR.__old-$packageArch" + RMDir /r "$R2" + RMDir /r "$R3" + CreateDirectory "$R2" + ClearErrors + SetOutPath "$R2"$1SetOutPath "$PLUGINSDIR" + ClearErrors + Rename "$R0" "$R3" + IfErrors 0 RenameStaged7za + ClearErrors + RMDir "$R0" + IfErrors 0 RenameStaged7za + IfFileExists "$R0\\*.*" HandleExtract7zaError RenameStaged7za + + RenameStaged7za: + ClearErrors + Rename "$R2" "$R0" + IfErrors HandleExtract7zaError DoneExtract7za + + HandleExtract7zaError:$2IfFileExists "$R0\\*.*" 0 RestoreOld7za + Goto ReportExtract7zaError + + RestoreOld7za: + IfFileExists "$R3\\*.*" 0 ReportExtract7zaError + Rename "$R3" "$R0" + + ReportExtract7zaError: + CreateDirectory "$R0" + SetOutPath "$R0" + RMDir /r "$R2" + RMDir /r "$R3"$3DoneExtract7za: + RMDir /r "$R2" + RMDir /r "$R3" +!macroend`, + 'StrCpy $R2 "$OUTDIR.__installing-$packageArch"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "migrate temp staged extraction to install-dir rename", + /Push \$OUTDIR\r?\n StrCpy \$R2 "\$TEMP\\MaiBotOneKeyInstall-\$packageArch"\r?\n RMDir \/r "\$R2"\r?\n CreateDirectory "\$R2"\r?\n ClearErrors\r?\n SetOutPath "\$R2"([\s\S]*?)CopyFiles "\$R2\\\*" \$OUTDIR([\s\S]*?)RMDir \/r "\$R2"([\s\S]*?)DoneExtract7za:\r?\n RMDir \/r "\$R2"\r?\n!macroend/u, + `Push $OUTDIR + StrCpy $R2 "$OUTDIR.__installing-$packageArch" + StrCpy $R3 "$OUTDIR.__old-$packageArch" + RMDir /r "$R2" + RMDir /r "$R3" + CreateDirectory "$R2" + ClearErrors + SetOutPath "$R2"$1SetOutPath "$PLUGINSDIR" + ClearErrors + Rename "$R0" "$R3" + IfErrors 0 RenameStaged7za + ClearErrors + RMDir "$R0" + IfErrors 0 RenameStaged7za + IfFileExists "$R0\\*.*" HandleExtract7zaError RenameStaged7za + + RenameStaged7za: + ClearErrors + Rename "$R2" "$R0" + IfErrors HandleExtract7zaError DoneExtract7za + + HandleExtract7zaError:$2IfFileExists "$R0\\*.*" 0 RestoreOld7za + Goto ReportExtract7zaError + + RestoreOld7za: + IfFileExists "$R3\\*.*" 0 ReportExtract7zaError + Rename "$R3" "$R0" + + ReportExtract7zaError: + CreateDirectory "$R0" + SetOutPath "$R0" + RMDir /r "$R2" + RMDir /r "$R3"$3DoneExtract7za: + RMDir /r "$R2" + RMDir /r "$R3" +!macroend`, + 'StrCpy $R2 "$OUTDIR.__installing-$packageArch"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "ensure direct-extract fallback targets install dir", + /HandleExtract7zaError:\r?\n IfErrors 0 DoneExtract7za([\s\S]*?)RMDir \/r "\$R2"\r?\n RMDir \/r "\$R3"\r?\n\r?\n Nsis7z::Extract "\$\{FILE\}"/u, + `HandleExtract7zaError: + IfErrors 0 DoneExtract7za$1CreateDirectory "$R0" + SetOutPath "$R0" + RMDir /r "$R2" + RMDir /r "$R3" + + Nsis7z::Extract "\${FILE}"`, + 'CreateDirectory "$R0"\n SetOutPath "$R0"\n RMDir /r "$R2"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "restore old install dir before retrying staged rename", + /HandleExtract7zaError:\r?\n IfErrors 0 DoneExtract7za\r?\n\r?\n DetailPrint/u, + `HandleExtract7zaError: + IfErrors 0 DoneExtract7za + + IfFileExists "$R0\\*.*" 0 RestoreOld7za + Goto ReportExtract7zaError + + RestoreOld7za: + IfFileExists "$R3\\*.*" 0 ReportExtract7zaError + Rename "$R3" "$R0" + + ReportExtract7zaError: + DetailPrint`, + 'RestoreOld7za:', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "handle pre-created empty install dir during staged rename", + /IfFileExists "\$OUTDIR\\\*\.\*" 0 RenameStaged7za\r?\n Rename "\$OUTDIR" "\$R3"\r?\n IfErrors 0 RenameStaged7za\r?\n Goto HandleExtract7zaError/u, + `ClearErrors + Rename "$OUTDIR" "$R3" + IfErrors 0 RenameStaged7za + ClearErrors + RMDir "$OUTDIR" + IfErrors 0 RenameStaged7za + IfFileExists "$OUTDIR\\*.*" HandleExtract7zaError RenameStaged7za`, + 'RMDir "$OUTDIR"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "remove unused staged-rename label", + /\r?\n PrepareTarget7za:/u, + "", + 'unused staged-rename label removed', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "avoid locking install dir before staged rename", + /(# Attempt to copy files in atomic way\r?\n) ClearErrors/u, + `$1 SetOutPath "$PLUGINSDIR" + ClearErrors`, + 'SetOutPath "$PLUGINSDIR"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "clear stale errors before final staged rename", + /(\r?\n RenameStaged7za:\r?\n) Rename "\$R2" "\$OUTDIR"/u, + `$1 ClearErrors + Rename "$R2" "$OUTDIR"`, + 'RenameStaged7za:\n ClearErrors', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "use saved install dir after changing SetOutPath", + /ClearErrors\r?\n Rename "\$OUTDIR" "\$R3"\r?\n IfErrors 0 RenameStaged7za\r?\n ClearErrors\r?\n RMDir "\$OUTDIR"\r?\n IfErrors 0 RenameStaged7za\r?\n IfFileExists "\$OUTDIR\\\*\.\*" HandleExtract7zaError RenameStaged7za([\s\S]*?)Rename "\$R2" "\$OUTDIR"([\s\S]*?)IfFileExists "\$OUTDIR\\\*\.\*" 0 RestoreOld7za([\s\S]*?)Rename "\$R3" "\$OUTDIR"([\s\S]*?)CreateDirectory "\$OUTDIR"\r?\n SetOutPath "\$OUTDIR"/u, + `ClearErrors + Rename "$R0" "$R3" + IfErrors 0 RenameStaged7za + ClearErrors + RMDir "$R0" + IfErrors 0 RenameStaged7za + IfFileExists "$R0\\*.*" HandleExtract7zaError RenameStaged7za$1Rename "$R2" "$R0"$2IfFileExists "$R0\\*.*" 0 RestoreOld7za$3Rename "$R3" "$R0"$4CreateDirectory "$R0" + SetOutPath "$R0"`, + 'Rename "$R2" "$R0"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "dedupe direct-extract fallback SetOutPath", + /CreateDirectory "\$R0"\r?\n SetOutPath "\$R0"\r?\n CreateDirectory "\$R0"\r?\n SetOutPath "\$R0"/u, + `CreateDirectory "$R0" + SetOutPath "$R0"`, + 'CreateDirectory "$R0"\n SetOutPath "$R0"\n RMDir /r "$R2"', + { optional: true }, +); + +await patchFile( + join(nsisTemplateRoot, "include", "extractAppPackage.nsh"), + "remove install-dir config preservation from staged install", + /RenameStaged7za:\r?\n ClearErrors\r?\n Rename "\$R2" "\$R0"\r?\n IfErrors HandleExtract7zaError PreserveUserConfigs7za\r?\n\r?\n PreserveUserConfigs7za:\r?\n(?: IfFileExists "\$R3\\resources\\modules\\napcat\\config\\\*\.\*" 0 \+3\r?\n CreateDirectory "\$R0\\resources\\modules\\napcat\\config"\r?\n CopyFiles \/SILENT "\$R3\\resources\\modules\\napcat\\config\\\*" "\$R0\\resources\\modules\\napcat\\config"\r?\n)?(?: IfFileExists "\$R3\\resources\\modules\\napcat\\napcat\\config\\\*\.\*" 0 \+3\r?\n CreateDirectory "\$R0\\resources\\modules\\napcat\\napcat\\config"\r?\n CopyFiles \/SILENT "\$R3\\resources\\modules\\napcat\\napcat\\config\\\*" "\$R0\\resources\\modules\\napcat\\napcat\\config"\r?\n)?(?: IfFileExists "\$R3\\resources\\modules\\SnowLuma\\config\\\*\.\*" 0 \+3\r?\n CreateDirectory "\$R0\\resources\\modules\\SnowLuma\\config"\r?\n CopyFiles \/SILENT "\$R3\\resources\\modules\\SnowLuma\\config\\\*" "\$R0\\resources\\modules\\SnowLuma\\config"\r?\n)?(?: IfFileExists "\$R3\\resources\\modules\\SnowLuma\\data\\\*\.\*" 0 \+3\r?\n CreateDirectory "\$R0\\resources\\modules\\SnowLuma\\data"\r?\n CopyFiles \/SILENT "\$R3\\resources\\modules\\SnowLuma\\data\\\*" "\$R0\\resources\\modules\\SnowLuma\\data"\r?\n)?(?: IfFileExists "\$R3\\resources\\modules\\SnowLuma\\logs\\\*\.\*" 0 \+3\r?\n CreateDirectory "\$R0\\resources\\modules\\SnowLuma\\logs"\r?\n CopyFiles \/SILENT "\$R3\\resources\\modules\\SnowLuma\\logs\\\*" "\$R0\\resources\\modules\\SnowLuma\\logs"\r?\n)? Goto DoneExtract7za/u, + `RenameStaged7za: + ClearErrors + Rename "$R2" "$R0" + IfErrors HandleExtract7zaError DoneExtract7za`, + "IfErrors HandleExtract7zaError DoneExtract7za", + { optional: true }, +); diff --git a/scripts/release/prepare-python-overrides.ts b/scripts/release/prepare-python-overrides.ts new file mode 100644 index 0000000..8b16c24 --- /dev/null +++ b/scripts/release/prepare-python-overrides.ts @@ -0,0 +1,155 @@ +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import process from "node:process"; + +const root = process.cwd(); +const target = join(root, "release-assets", "python-overrides"); +const tunaIndex = "https://pypi.tuna.tsinghua.edu.cn/simple"; + +function pythonPath(): string { + const candidates = [ + join(root, "runtime", "python", "python.exe"), + join(root, "runtime", "python", "bin", "python.exe"), + join(root, "runtime", "python", "python"), + join(root, "runtime", "python", "bin", "python3"), + ]; + const path = candidates.find((candidate) => existsSync(candidate)); + if (!path) { + throw new Error("Cannot find bundled Python in runtime/python."); + } + return path; +} + +function run(command: string, args: string[], env?: Record): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: root, + env: { + ...process.env, + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + ...env, + }, + shell: false, + stdio: ["ignore", "pipe", "pipe"], + }); + const output: string[] = []; + + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + process.stdout.write(chunk); + output.push(chunk); + }); + child.stderr.on("data", (chunk: string) => { + process.stderr.write(chunk); + output.push(chunk); + }); + child.on("error", reject); + child.on("exit", (code) => { + if (code === 0) { + resolve(output.join("")); + return; + } + reject(new Error(`${command} exited with code ${code ?? "unknown"}`)); + }); + }); +} + +async function readMaiBotDependencies(python: string): Promise { + const pyprojectPath = join(root, "modules", "MaiBot", "pyproject.toml"); + const requirementsPath = join(root, "modules", "MaiBot", "requirements.txt"); + if (existsSync(pyprojectPath)) { + const script = String.raw` +import json +import pathlib +import sys +import tomllib + +data = tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) +deps = data.get("project", {}).get("dependencies", []) +print(json.dumps([item for item in deps if isinstance(item, str) and item.strip()], ensure_ascii=False)) +`; + const output = await run(python, ["-c", script, pyprojectPath]); + const line = output.split(/\r?\n/u).find((item) => item.trim().startsWith("[")); + const parsed = JSON.parse(line ?? "[]") as unknown; + if (Array.isArray(parsed) && parsed.length > 0) { + return parsed.filter((item): item is string => typeof item === "string" && item.trim().length > 0); + } + } + + if (!existsSync(requirementsPath)) { + throw new Error(`Cannot find MaiBot dependency file: ${pyprojectPath} or ${requirementsPath}`); + } + + const content = await readFile(requirementsPath, "utf8"); + return content + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith("#")) + .filter((line) => !line.startsWith("-") && !line.startsWith("git+") && !/^https?:\/\//iu.test(line)) + .map((line) => line.replace(/\s+#.*$/u, "").trim()) + .filter(Boolean); +} + +async function prepareEmptyTarget(): Promise { + await rm(target, { recursive: true, force: true }); + await mkdir(target, { recursive: true }); + await writeFile(join(target, ".keep"), "", "utf8"); +} + +async function main(): Promise { + const mode = process.argv[2] ?? "basic"; + await prepareEmptyTarget(); + if (mode === "basic") { + console.log("[release] Prepared empty python-overrides for basic installer"); + return; + } + if (mode !== "full") { + throw new Error(`Unknown python-overrides mode: ${mode}`); + } + + const python = pythonPath(); + const dependencies = Array.from(new Set([ + ...(await readMaiBotDependencies(python)), + "maibot-dashboard", + "maim-message", + ])); + if (dependencies.length === 0) { + throw new Error("No dependencies resolved for full python-overrides build."); + } + + await mkdir(dirname(target), { recursive: true }); + console.log(`[release] Installing ${dependencies.length} dependency specifier(s) into ${target}`); + await run(python, [ + "-m", + "pip", + "install", + "--upgrade", + "--upgrade-strategy", + "only-if-needed", + "--target", + target, + "--timeout", + "120", + "--retries", + "5", + "--disable-pip-version-check", + "--no-compile", + "--no-warn-script-location", + "--progress-bar", + "off", + "--index-url", + tunaIndex, + "--trusted-host", + "pypi.tuna.tsinghua.edu.cn", + ...dependencies, + ], { + PYTHONPATH: target, + }); + await rm(join(target, ".keep"), { force: true }); +} + +await main(); diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..3922c4c --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,257 @@ +import { app, BrowserWindow, Menu, nativeImage, shell, Tray } from "electron"; +import { join } from "node:path"; +import { registerAppIpc } from "./ipc/app"; +import { registerPtyIpc } from "./ipc/pty"; +import { PtySessionManager } from "./pty/pty-session-manager"; +import { InitManager } from "./services/init-manager"; +import { acquireInstallInstanceLock } from "./services/instance-lock"; +import { LogStore } from "./services/log-store"; +import { ModuleUpdater } from "./services/module-updater"; +import { configureRuntimePaths } from "./services/paths"; +import { PythonDependencyManager } from "./services/python-dependency-manager"; +import { ResourceLocationManager } from "./services/resource-location-manager"; +import { ServiceManager } from "./services/service-manager"; + +const runtimePaths = configureRuntimePaths(); +const instanceLock = acquireInstallInstanceLock(runtimePaths); +const resourceLocationManager = new ResourceLocationManager(runtimePaths, app.isPackaged); +const resourceLock = instanceLock.acquired + ? resourceLocationManager.acquireInitialLock() + : { acquired: true }; +const logStore = new LogStore(runtimePaths); +const initManager = new InitManager(runtimePaths); +const moduleUpdater = new ModuleUpdater(runtimePaths, initManager); +const pythonDependencyManager = new PythonDependencyManager(runtimePaths, initManager); +const ptySessionManager = new PtySessionManager(); +const serviceManager = new ServiceManager(runtimePaths, initManager, logStore, ptySessionManager, pythonDependencyManager); + +let mainWindow: BrowserWindow | null = null; +let tray: Tray | null = null; +let appIpcDisposables: ReturnType | null = null; +let allowQuit = false; +let quitRequested = false; + +function createFallbackIcon(): Electron.NativeImage { + return nativeImage.createFromDataURL( + "data:image/svg+xml;utf8," + + encodeURIComponent( + ` + + + + + `, + ), + ); +} + +function createAppIcon(): Electron.NativeImage { + const iconPath = app.isPackaged + ? join(process.resourcesPath, "icon.png") + : join(process.cwd(), "resources", "icon.png"); + const icon = nativeImage.createFromPath(iconPath); + return icon.isEmpty() ? createFallbackIcon() : icon; +} + +function broadcastWindowState(window: BrowserWindow): void { + if (window.isDestroyed()) { + return; + } + + window.webContents.send("desktop:window-state", { + isMaximized: window.isMaximized(), + isFullScreen: window.isFullScreen(), + isFocused: window.isFocused(), + }); +} + +function createMainWindow(): BrowserWindow { + const window = new BrowserWindow({ + title: "MaiBot OneKey", + width: 1280, + height: 820, + minWidth: 1080, + minHeight: 720, + show: false, + backgroundColor: "#00000000", + transparent: true, + frame: false, + icon: createAppIcon(), + titleBarStyle: process.platform === "darwin" ? "hidden" : "default", + trafficLightPosition: process.platform === "darwin" ? { x: -100, y: -100 } : undefined, + webPreferences: { + preload: join(__dirname, "../preload/index.mjs"), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + webviewTag: true, + }, + }); + + window.once("ready-to-show", () => { + window.show(); + broadcastWindowState(window); + }); + + window.on("close", (event) => { + if (allowQuit) { + return; + } + + event.preventDefault(); + window.webContents.send("desktop:close-request"); + }); + + window.on("closed", () => { + if (mainWindow === window) { + mainWindow = null; + } + }); + + window.on("maximize", () => broadcastWindowState(window)); + window.on("unmaximize", () => broadcastWindowState(window)); + window.on("enter-full-screen", () => broadcastWindowState(window)); + window.on("leave-full-screen", () => broadcastWindowState(window)); + window.on("focus", () => broadcastWindowState(window)); + window.on("blur", () => broadcastWindowState(window)); + window.on("restore", () => broadcastWindowState(window)); + + window.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url).catch(() => undefined); + return { action: "deny" }; + }); + + if (process.env.ELECTRON_RENDERER_URL) { + window.loadURL(process.env.ELECTRON_RENDERER_URL).catch(() => undefined); + } else { + window.loadFile(join(__dirname, "../renderer/index.html")).catch(() => undefined); + } + + return window; +} + +function showMainWindow(): void { + if (!mainWindow || mainWindow.isDestroyed()) { + mainWindow = createMainWindow(); + return; + } + + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + + mainWindow.show(); + mainWindow.focus(); +} + +function requestQuit(): void { + if (quitRequested) { + return; + } + + quitRequested = true; + allowQuit = true; + logStore.append("desktop", "system", "quit requested, shutting down managed services"); + void serviceManager + .shutdownAll() + .catch((error: unknown) => { + logStore.append("desktop", "system", `service shutdown failed: ${String(error)}`); + }) + .finally(() => { + app.quit(); + }); +} + +function createTray(): Tray { + const nextTray = new Tray(createAppIcon().resize({ width: 32, height: 32 })); + + const withLog = + (action: () => Promise) => + (): void => { + void action().catch((error: unknown) => { + logStore.append("desktop", "system", String(error)); + }); + }; + + nextTray.setToolTip("MaiBot OneKey"); + nextTray.setContextMenu( + Menu.buildFromTemplate([ + { label: "\u663e\u793a MaiBot OneKey", click: showMainWindow }, + { type: "separator" }, + { label: "\u542f\u52a8\u5168\u90e8\u670d\u52a1", click: withLog(() => serviceManager.startAll()) }, + { label: "\u505c\u6b62\u5168\u90e8\u670d\u52a1", click: withLog(() => serviceManager.stopAll()) }, + { label: "\u6253\u5f00\u65e5\u5fd7\u76ee\u5f55", click: withLog(() => shell.openPath(runtimePaths.logsRoot)) }, + { type: "separator" }, + { label: "\u5173\u95ed\u5e94\u7528", click: requestQuit }, + ]), + ); + nextTray.on("double-click", showMainWindow); + return nextTray; +} + +if (!instanceLock.acquired || !resourceLock.acquired) { + if (!resourceLock.acquired) { + logStore.append( + "desktop", + "system", + `runtime resource path is locked by pid ${resourceLock.existing?.pid ?? "unknown"}`, + ); + } + resourceLocationManager.release(); + serviceManager.dispose(); + ptySessionManager.dispose(); + app.quit(); +} else { + app.whenReady().then(() => { + mainWindow = createMainWindow(); + tray = createTray(); + + appIpcDisposables = registerAppIpc({ + paths: runtimePaths, + initManager, + moduleUpdater, + pythonDependencyManager, + resourceLocationManager, + serviceManager, + logStore, + getMainWindow: () => mainWindow, + requestQuit, + showMainWindow, + }); + registerPtyIpc({ + manager: ptySessionManager, + getMainWindow: () => mainWindow, + }); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createMainWindow(); + } else { + showMainWindow(); + } + }); + }); + + app.on("window-all-closed", () => { + requestQuit(); + }); + + app.on("before-quit", (event) => { + if (!quitRequested) { + event.preventDefault(); + requestQuit(); + return; + } + + allowQuit = true; + tray?.destroy(); + appIpcDisposables?.dispose(); + serviceManager.dispose(); + ptySessionManager.dispose(); + }); + + app.on("will-quit", () => { + resourceLocationManager.release(); + instanceLock.release(); + }); +} diff --git a/src/main/ipc/app.ts b/src/main/ipc/app.ts new file mode 100644 index 0000000..457a14f --- /dev/null +++ b/src/main/ipc/app.ts @@ -0,0 +1,1561 @@ +import { app, BrowserWindow, dialog, ipcMain, screen, shell } from "electron"; +import { execFile } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdir, readdir, readFile, rename, rm, stat } from "node:fs/promises"; +import { isAbsolute, join, relative, resolve, sep } from "node:path"; +import type { + CloseAction, + DesktopSnapshot, + InitRepairResult, + InitState, + LauncherResetResult, + LogEntry, + LocalChatConnectionState, + LocalChatConnectRequest, + LocalChatMessageEvent, + LocalChatSendRequest, + MaiBotConfigFileName, + MaiBotConfigImportResult, + MaiBotDataImportResult, + MaiBotDataResetResult, + MaiBotInstalledPlugin, + MaiBotPluginConfigSaveResult, + MaiBotPluginConfigState, + MaiBotPluginConfigValue, + MaiBotPluginListOptions, + MaiBotPluginListResult, + MaiBotPluginOperationRequest, + MaiBotPluginOperationResult, + MaiBotPluginReadmeResult, + MaiBotPluginStats, + MaiBotStatisticSummary, + ManagedPythonPackageName, + ModuleRuntimeVersions, + ModuleUpdateResult, + ModuleSourceConfig, + ModuleSourceUpdate, + ModuleTagOption, + PythonOverridesState, + PythonPackageSourcePreset, + PythonRuntimeCandidate, + PythonPackageInstallRequest, + PythonPackageInstallResult, + PythonPackageVersionList, + QqBackend, + QqAccountSetupRequest, + RuntimePaths, + RuntimePathConfig, + RuntimePathKey, + RuntimePathUpdate, + RuntimeResourcePathChangeResult, + RuntimeResourcePathKey, + ServiceCommandUpdate, + ServiceDescriptor, + ServiceId, + SnowLumaResetResult, + StartupAgreementConfirmResult, + StartupAgreementState, + TerminalSettings, + WindowState, +} from "../../shared/contracts"; +import { InitManager } from "../services/init-manager"; +import { LogStore } from "../services/log-store"; +import { LocalChatAdapter } from "../services/local-chat-adapter"; +import { MaiBotPluginClient } from "../services/maibot-plugin-client"; +import { ModuleUpdater } from "../services/module-updater"; +import { PythonDependencyManager } from "../services/python-dependency-manager"; +import { ResourceLocationManager } from "../services/resource-location-manager"; +import { ServiceManager } from "../services/service-manager"; + +const LAUNCHER_SETTING_FILES = [ + "resource-paths.json", + "resource-location.json", + "service-commands.json", + "runtime-paths.json", + "terminal-settings.json", + "qq-backend.json", + "message-platform.json", + "module-sources.json", + "python-dependency-source.json", +]; +const LAUNCHER_RUNTIME_DIRECTORIES = ["modules", "python-overrides", "logs"]; +const RETIRED_ENTRY_DIRECTORY = ".reset-pending-delete"; +const REMOVE_RETRY_OPTIONS = { recursive: true, force: true, maxRetries: 8, retryDelay: 250 } as const; +const NORMAL_MINIMUM_SIZE = { width: 1080, height: 720 }; +const FLOATING_BALL_SIZE = { width: 96, height: 96 }; +const FLOATING_PANEL_SIZE = { width: 380, height: 520 }; +const FLOATING_STRIP_SIZE = { width: 28, height: 112 }; +const FLOATING_EDGE_SNAP_DISTANCE = 18; + +interface RegisterAppIpcOptions { + paths: RuntimePaths; + initManager: InitManager; + moduleUpdater: ModuleUpdater; + pythonDependencyManager: PythonDependencyManager; + resourceLocationManager: ResourceLocationManager; + serviceManager: ServiceManager; + logStore: LogStore; + getMainWindow: () => BrowserWindow | null; + requestQuit: () => void; + showMainWindow: () => void; +} + +export interface RegisteredAppIpcDisposables { + localChatAdapter: LocalChatAdapter; + dispose: () => void; +} + +function readWindowState( + window: BrowserWindow | null, + isFloating = false, + floatingEdgeSide: "left" | "right" | null = null, +): WindowState { + if (!window || window.isDestroyed()) { + return { + isMaximized: false, + isFullScreen: false, + isFocused: false, + isFloating, + isFloatingCollapsed: Boolean(floatingEdgeSide), + floatingEdge: floatingEdgeSide ?? undefined, + }; + } + + return { + isMaximized: window.isMaximized(), + isFullScreen: window.isFullScreen(), + isFocused: window.isFocused(), + isFloating, + isFloatingCollapsed: Boolean(floatingEdgeSide), + floatingEdge: floatingEdgeSide ?? undefined, + }; +} + +function runProcess(file: string, args: string[], cwd: string, env?: Record): Promise { + return new Promise((resolve) => { + execFile( + file, + args, + { + cwd, + timeout: 10_000, + windowsHide: true, + maxBuffer: 1024 * 1024, + env: { + ...process.env, + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + ...env, + }, + }, + (error, stdout, stderr) => { + if (error) { + resolve(undefined); + return; + } + resolve(`${stdout}${stderr}`.trim() || undefined); + }, + ); + }); +} + +function isRuntimeBusy(service: ServiceDescriptor): boolean { + return service.status === "starting" || service.status === "running" || service.status === "stopping"; +} + +function normalizePathForCompare(path: string): string { + const resolved = resolve(path); + return process.platform === "win32" ? resolved.toLowerCase() : resolved; +} + +function samePath(left: string, right: string): boolean { + return normalizePathForCompare(left) === normalizePathForCompare(right); +} + +function isPathInside(parent: string, child: string): boolean { + const resolvedParent = resolve(parent); + const resolvedChild = resolve(child); + const diff = relative(resolvedParent, resolvedChild); + return Boolean(diff) && diff !== ".." && !diff.startsWith(`..${sep}`) && !isAbsolute(diff); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function isBusyFsError(error: unknown): boolean { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + return code === "EBUSY" || code === "EPERM" || code === "ENOTEMPTY"; +} + +async function removePathWithRetry(path: string): Promise { + try { + await rm(path, REMOVE_RETRY_OPTIONS); + return; + } catch (error) { + if (!isBusyFsError(error)) { + throw error; + } + } + + for (let attempt = 0; attempt < 5; attempt++) { + await sleep(500 + attempt * 500); + try { + await rm(path, REMOVE_RETRY_OPTIONS); + return; + } catch (error) { + if (!isBusyFsError(error) || attempt === 4) { + throw error; + } + } + } +} + +async function retireAndRemovePath(path: string, root: string): Promise { + try { + await removePathWithRetry(path); + return; + } catch (error) { + if (!isBusyFsError(error)) { + throw error; + } + } + + const retiredRoot = join(root, RETIRED_ENTRY_DIRECTORY); + await mkdir(retiredRoot, { recursive: true }); + const retiredPath = join(retiredRoot, `${Date.now()}-${Math.random().toString(16).slice(2)}`); + + try { + await rename(path, retiredPath); + } catch (error) { + if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") { + return; + } + throw error; + } + + void removePathWithRetry(retiredPath).catch(() => undefined); +} + +async function removeExistingPath(path: string): Promise { + if (!existsSync(path)) { + return []; + } + await removePathWithRetry(path); + return [path]; +} + +async function clearDirectoryContents(root: string, entryNames?: string[]): Promise { + if (!existsSync(root)) { + await mkdir(root, { recursive: true }); + return []; + } + + const entries = entryNames ?? (await readdir(root)); + const removedEntries = entries.map((entry) => join(root, entry)); + await Promise.all(removedEntries.map((entryPath) => retireAndRemovePath(entryPath, root))); + await mkdir(root, { recursive: true }); + return removedEntries; +} + +interface ParsedVersionTag { + tag: string; + parts: number[]; + prerelease: boolean; +} + +async function readPyprojectVersion(path: string): Promise { + try { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(path, "utf8"); + return content.match(/^\s*version\s*=\s*["']([^"']+)["']/mu)?.[1]; + } catch { + return undefined; + } +} + +function normalizePythonPackageName(name: string): string { + return name.toLowerCase().replace(/[-_.]+/gu, "-"); +} + +function normalizePythonVersion(version: string): number[] { + const clean = version.trim().toLowerCase().replace(/^v/u, "").split(/[+-]/u, 1)[0]; + return clean.split(/[._-]/u).map((part) => { + const value = part.match(/^\d+/u)?.[0]; + return value ? Number(value) : 0; + }); +} + +function comparePythonVersions(left: string, right: string): number { + const leftParts = normalizePythonVersion(left); + const rightParts = normalizePythonVersion(right); + const length = Math.max(leftParts.length, rightParts.length); + for (let index = 0; index < length; index++) { + const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); + if (diff !== 0) { + return diff; + } + } + return left.localeCompare(right, "en-US", { numeric: true, sensitivity: "base" }); +} + +async function readPythonDistInfoVersion(root: string, packageName: string): Promise { + try { + const { readdir, readFile } = await import("node:fs/promises"); + const expectedName = normalizePythonPackageName(packageName); + const versions: string[] = []; + const entries = await readdir(root, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory() || !entry.name.endsWith(".dist-info")) { + continue; + } + const metadata = await readFile(join(root, entry.name, "METADATA"), "utf8").catch(() => undefined); + if (!metadata) { + continue; + } + const name = metadata.match(/^Name:\s*(.+)$/imu)?.[1]?.trim(); + if (!name || normalizePythonPackageName(name) !== expectedName) { + continue; + } + const version = metadata.match(/^Version:\s*(.+)$/imu)?.[1]?.trim(); + if (version) { + versions.push(version); + } + } + return versions.sort(comparePythonVersions).at(-1); + } catch { + return undefined; + } +} + +function parseVersionTag(tag: string): ParsedVersionTag | undefined { + const normalized = tag.replace(/^v/iu, ""); + const match = normalized.match(/^(\d+(?:\.\d+){0,3})(?:[-._]?([a-z]+)\.?(\d*)?)?$/iu); + if (!match) { + return undefined; + } + + return { + tag, + parts: match[1].split(".").map((part) => Number(part)), + prerelease: Boolean(match[2]), + }; +} + +function compareParsedTags(left: ParsedVersionTag, right: ParsedVersionTag): number { + const length = Math.max(left.parts.length, right.parts.length); + for (let index = 0; index < length; index++) { + const diff = (left.parts[index] ?? 0) - (right.parts[index] ?? 0); + if (diff !== 0) { + return diff; + } + } + return left.tag.localeCompare(right.tag, "en-US", { numeric: true, sensitivity: "base" }); +} + +function isAtLeastVersion(tag: ParsedVersionTag, minimum: number[]): boolean { + const length = Math.max(tag.parts.length, minimum.length); + for (let index = 0; index < length; index++) { + const diff = (tag.parts[index] ?? 0) - (minimum[index] ?? 0); + if (diff !== 0) { + return diff > 0; + } + } + return true; +} + +function pickLatestTags( + rawTags: string[], +): Pick { + const parsed = rawTags.map(parseVersionTag).filter((tag): tag is ParsedVersionTag => Boolean(tag)); + const standard = parsed.filter((tag) => isAtLeastVersion(tag, [1, 0, 0])); + const stable = standard.filter((tag) => !tag.prerelease).sort(compareParsedTags).at(-1)?.tag; + const prerelease = standard.filter((tag) => tag.prerelease).sort(compareParsedTags).at(-1)?.tag; + const legacy = parsed.filter((tag) => !isAtLeastVersion(tag, [1, 0, 0])).sort(compareParsedTags).at(-1)?.tag; + return { + maibotLatestStableTag: stable, + maibotLatestPrereleaseTag: prerelease, + maibotLatestLegacyTag: legacy, + }; +} + +function parsePackageVersion(version: string): ParsedVersionTag | undefined { + const normalized = version.replace(/^v/iu, ""); + const match = normalized.match(/^(\d+(?:\.\d+){0,3})(?:(?:[-._]?(?:dev|a|alpha|b|beta|rc|pre|preview))\d*)?/iu); + if (!match) { + return undefined; + } + + return { + tag: version, + parts: match[1].split(".").map((part) => Number(part)), + prerelease: /(?:^|[._+-])(?:dev|a|alpha|b|beta|rc|pre|preview)\d*/iu.test(version), + }; +} + +async function fetchPypiVersionSummary( + packageName: string, +): Promise> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + try { + const response = await fetch(`https://pypi.org/pypi/${packageName}/json`, { signal: controller.signal }); + if (!response.ok) { + return {}; + } + const data = (await response.json()) as { info?: { version?: unknown }; releases?: Record }; + const latestPypi = typeof data.info?.version === "string" ? data.info.version : undefined; + const parsed = Object.keys(data.releases ?? {}) + .map(parsePackageVersion) + .filter((version): version is ParsedVersionTag => Boolean(version)); + const stable = parsed.filter((version) => !version.prerelease).sort(compareParsedTags).at(-1)?.tag; + const prerelease = parsed.filter((version) => version.prerelease).sort(compareParsedTags).at(-1)?.tag; + return { + dashboardLatestPypi: latestPypi ?? stable, + dashboardLatestStablePypi: stable, + dashboardLatestPrereleasePypi: prerelease, + }; + } catch { + return {}; + } finally { + clearTimeout(timeout); + } +} + +function decodeStatisticText(content: string): string { + return content + .replace(/\u001b\[[0-9;]*m/gu, "") + .replace(//giu, "\n") + .replace(/<\/(?:p|div|tr|li|h[1-6]|section|article)>/giu, "\n") + .replace(/<[^>]+>/gu, " ") + .replace(/ /giu, " ") + .replace(/¥/giu, "¥") + .replace(/&/giu, "&") + .replace(/</giu, "<") + .replace(/>/giu, ">") + .replace(/&#(\d+);/gu, (_match, code: string) => String.fromCodePoint(Number(code))) + .replace(/\r\n?/gu, "\n"); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function readStatisticField(text: string, label: string): string | undefined { + const inlineValue = text.match(new RegExp(`${escapeRegExp(label)}\\s*[::]\\s*([^\\n]+)`, "u"))?.[1]?.trim(); + if (inlineValue) { + return inlineValue; + } + + const lines = text.split("\n").map((line) => line.trim()).filter(Boolean); + const labelIndex = lines.findIndex((line) => line === label); + const nextLine = labelIndex >= 0 ? lines[labelIndex + 1] : undefined; + return nextLine && !nextLine.endsWith(":") && !nextLine.endsWith(":") ? nextLine : undefined; +} + +function readStatisticCount(text: string, label: string): number | undefined { + const raw = readStatisticField(text, label); + const value = raw?.match(/[\d,]+/u)?.[0]?.replace(/,/gu, ""); + return value ? Number(value) : undefined; +} + +function parseChatStatistics(text: string): MaiBotStatisticSummary["chatStats"] { + const lines = text.split("\n").map((line) => line.trim()).filter(Boolean); + const startIndex = lines.findIndex((line) => line.includes("聊天消息统计")); + if (startIndex < 0) { + return []; + } + + const stats: MaiBotStatisticSummary["chatStats"] = []; + for (const line of lines.slice(startIndex + 1)) { + if (line.startsWith("-") || line.includes("Token/") || line.includes("花费/")) { + break; + } + if (line.includes("联系人") || line.includes("群组名称") || line.includes("消息数量")) { + continue; + } + const match = line.match(/^(.+?)\s+(\d+)$/u); + if (!match) { + continue; + } + stats.push({ name: match[1].trim(), messageCount: Number(match[2]) }); + } + return stats; +} + +async function readMaiBotStatistics(paths: RuntimePaths): Promise { + const candidates = [ + join(paths.maibotRoot, "maibot_statistics.html"), + join(paths.maibotRoot, "data", "maibot_statistics.html"), + join(paths.maibotRoot, "logs", "maibot_statistics.html"), + ]; + const sourcePath = candidates.find((path) => existsSync(path)); + if (!sourcePath) { + return { available: false, updatedAt: Date.now(), chatStats: [] }; + } + + const [content, fileStat] = await Promise.all([readFile(sourcePath, "utf8"), stat(sourcePath)]); + const text = decodeStatisticText(content); + const periodLabel = text.match(/(最近[^\s((]*统计数据)/u)?.[1]; + const startedAt = text.match(/自\s*([^,,))]+)开始/u)?.[1]?.trim(); + + return { + available: true, + updatedAt: fileStat.mtimeMs, + sourcePath, + periodLabel, + startedAt, + totalOnlineTime: readStatisticField(text, "总在线时间"), + totalMessages: readStatisticCount(text, "总消息数"), + totalReplies: readStatisticCount(text, "总回复数"), + totalRequests: readStatisticCount(text, "总请求数"), + totalTokens: readStatisticCount(text, "总Token数"), + totalCost: readStatisticField(text, "总花费"), + costPerMessage: readStatisticField(text, "花费/消息数量"), + costPerReceivedMessage: readStatisticField(text, "花费/接受消息数量"), + costPerReply: readStatisticField(text, "花费/回复数量"), + costPerHour: readStatisticField(text, "花费/时间"), + tokensPerHour: readStatisticField(text, "Token/时间"), + chatStats: parseChatStatistics(text), + }; +} + +export function registerAppIpc({ + paths, + initManager, + moduleUpdater, + pythonDependencyManager, + resourceLocationManager, + serviceManager, + logStore, + getMainWindow, + requestQuit, + showMainWindow, +}: RegisterAppIpcOptions): RegisteredAppIpcDisposables { + let remoteModuleVersionsCache: ModuleRuntimeVersions = {}; + let remoteModuleVersionsRefreshPromise: Promise | null = null; + let initDependencyRefreshPromise: Promise | null = null; + let floatingMode = false; + let floatingPanelExpanded = false; + let floatingEdgeSide: "left" | "right" | null = null; + let normalBounds: Electron.Rectangle | null = null; + + const sendWindowState = (window: BrowserWindow | null): WindowState => { + const state = readWindowState(window, floatingMode, floatingEdgeSide); + window?.webContents.send("desktop:window-state", state); + return state; + }; + + const clampFloatingBounds = (bounds: Electron.Rectangle): Electron.Rectangle => { + const display = screen.getDisplayMatching(bounds); + const workArea = display.workArea; + return { + x: Math.min(Math.max(bounds.x, workArea.x), workArea.x + workArea.width - bounds.width), + y: Math.min(Math.max(bounds.y, workArea.y), workArea.y + workArea.height - bounds.height), + width: bounds.width, + height: bounds.height, + }; + }; + + const floatingBounds = (window: BrowserWindow, size: { width: number; height: number }): Electron.Rectangle => { + const display = screen.getDisplayMatching(window.getBounds()); + return { + x: Math.round(display.workArea.x + display.workArea.width - size.width - 18), + y: Math.round(display.workArea.y + display.workArea.height - size.height - 18), + width: size.width, + height: size.height, + }; + }; + + const applyFloatingMode = (enabled: boolean): WindowState => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return readWindowState(window, floatingMode, floatingEdgeSide); + } + + if (enabled && !floatingMode) { + normalBounds = window.getBounds(); + if (window.isMaximized()) { + window.unmaximize(); + } + floatingMode = true; + floatingPanelExpanded = false; + floatingEdgeSide = null; + window.setMinimumSize(72, 72); + window.setResizable(false); + window.setAlwaysOnTop(true, "floating"); + window.setBounds(floatingBounds(window, FLOATING_BALL_SIZE), true); + window.show(); + window.focus(); + return sendWindowState(window); + } + + if (!enabled && floatingMode) { + floatingMode = false; + floatingPanelExpanded = false; + floatingEdgeSide = null; + window.setAlwaysOnTop(false); + window.setResizable(true); + window.setMinimumSize(NORMAL_MINIMUM_SIZE.width, NORMAL_MINIMUM_SIZE.height); + window.setBounds(normalBounds ?? { x: 80, y: 80, width: 1280, height: 820 }, true); + normalBounds = null; + window.show(); + window.focus(); + return sendWindowState(window); + } + + return sendWindowState(window); + }; + + const applyFloatingPanelExpanded = (expanded: boolean): WindowState => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return readWindowState(window, floatingMode, floatingEdgeSide); + } + if (!floatingMode) { + return sendWindowState(window); + } + floatingPanelExpanded = expanded; + floatingEdgeSide = null; + const currentBounds = window.getBounds(); + const nextSize = expanded ? FLOATING_PANEL_SIZE : FLOATING_BALL_SIZE; + window.setBounds( + clampFloatingBounds({ + x: currentBounds.x, + y: currentBounds.y, + width: nextSize.width, + height: nextSize.height, + }), + true, + ); + return sendWindowState(window); + }; + + const moveFloatingBy = (deltaX: number, deltaY: number): WindowState => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return readWindowState(window, floatingMode, floatingEdgeSide); + } + if (!floatingMode) { + return sendWindowState(window); + } + + const currentBounds = window.getBounds(); + if (floatingEdgeSide) { + const previousEdgeSide = floatingEdgeSide; + floatingEdgeSide = null; + const undockedBounds = { + x: previousEdgeSide === "left" ? currentBounds.x : currentBounds.x + currentBounds.width - FLOATING_BALL_SIZE.width, + y: currentBounds.y, + width: FLOATING_BALL_SIZE.width, + height: FLOATING_BALL_SIZE.height, + }; + window.setBounds(clampFloatingBounds(undockedBounds), false); + } + + const bounds = window.getBounds(); + window.setBounds( + clampFloatingBounds({ + ...bounds, + x: bounds.x + Math.round(deltaX), + y: bounds.y + Math.round(deltaY), + }), + false, + ); + return sendWindowState(window); + }; + + const moveFloatingTo = (screenX: number, screenY: number, offsetX: number, offsetY: number): WindowState => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return readWindowState(window, floatingMode, floatingEdgeSide); + } + if (!floatingMode) { + return sendWindowState(window); + } + + const wasEdgeDocked = Boolean(floatingEdgeSide); + if (floatingEdgeSide) { + floatingEdgeSide = null; + const currentBounds = window.getBounds(); + const nextSize = floatingPanelExpanded ? FLOATING_PANEL_SIZE : FLOATING_BALL_SIZE; + window.setBounds( + clampFloatingBounds({ + x: currentBounds.x, + y: currentBounds.y, + width: nextSize.width, + height: nextSize.height, + }), + false, + ); + } + + const bounds = window.getBounds(); + const safeOffsetX = wasEdgeDocked && !floatingPanelExpanded + ? Math.round(FLOATING_BALL_SIZE.width / 2) + : Math.min(Math.max(Math.round(offsetX), 0), bounds.width); + const safeOffsetY = wasEdgeDocked && !floatingPanelExpanded + ? Math.round(FLOATING_BALL_SIZE.height / 2) + : Math.min(Math.max(Math.round(offsetY), 0), bounds.height); + window.setBounds( + clampFloatingBounds({ + ...bounds, + x: Math.round(screenX) - safeOffsetX, + y: Math.round(screenY) - safeOffsetY, + }), + false, + ); + return sendWindowState(window); + }; + + const finishFloatingDrag = (): WindowState => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return readWindowState(window, floatingMode, floatingEdgeSide); + } + if (!floatingMode) { + return sendWindowState(window); + } + + const currentBounds = window.getBounds(); + const display = screen.getDisplayMatching(currentBounds); + const workArea = display.workArea; + const isNearLeft = currentBounds.x <= workArea.x + FLOATING_EDGE_SNAP_DISTANCE; + const isNearRight = + currentBounds.x + currentBounds.width >= workArea.x + workArea.width - FLOATING_EDGE_SNAP_DISTANCE; + + if (!floatingPanelExpanded && (isNearLeft || isNearRight)) { + floatingEdgeSide = isNearLeft ? "left" : "right"; + const x = floatingEdgeSide === "left" + ? workArea.x + : workArea.x + workArea.width - FLOATING_STRIP_SIZE.width; + window.setBounds( + clampFloatingBounds({ + x, + y: currentBounds.y, + width: FLOATING_STRIP_SIZE.width, + height: FLOATING_STRIP_SIZE.height, + }), + true, + ); + return sendWindowState(window); + } + + floatingEdgeSide = null; + window.setBounds(clampFloatingBounds(currentBounds), true); + return sendWindowState(window); + }; + + const readLocalModuleVersions = async (): Promise => { + const versions: ModuleRuntimeVersions = {}; + const maibotRoot = paths.maibotRoot; + + const pyprojectVersion = await readPyprojectVersion(join(maibotRoot, "pyproject.toml")); + if (pyprojectVersion) { + versions.maibotLocal = pyprojectVersion; + versions.maibotLocalSource = "pyproject"; + } + + const dashboardVersion = await readPythonDistInfoVersion( + pythonDependencyManager.getOverridesRoot(), + "maibot-dashboard", + ); + if (dashboardVersion) { + versions.dashboardOverride = dashboardVersion; + versions.dashboardOverrideSource = "python-overrides"; + } + + return versions; + }; + + const readRemoteModuleVersions = async (): Promise => { + const versions: ModuleRuntimeVersions = {}; + const gitPath = initManager.getGitPath(); + const sourceConfig = await moduleUpdater.getSourceConfig().catch(() => undefined); + const tagRemoteUrls = [sourceConfig?.maibotUrl, "https://github.com/Mai-with-u/MaiBot.git"].filter( + (url, index, urls): url is string => Boolean(url && urls.indexOf(url) === index), + ); + if (existsSync(gitPath)) { + for (const remoteUrl of tagRemoteUrls) { + const tagsOutput = await runProcess(gitPath, ["ls-remote", "--tags", remoteUrl], paths.installRoot); + if (!tagsOutput) { + continue; + } + const tags = tagsOutput + .split(/\r?\n/u) + .map((line) => line.match(/refs\/tags\/(.+?)(?:\^\{\})?$/u)?.[1]) + .filter((tag): tag is string => Boolean(tag)); + Object.assign(versions, pickLatestTags(Array.from(new Set(tags)))); + versions.maibotRemoteSource = remoteUrl; + break; + } + } + + const dashboardVersions = await fetchPypiVersionSummary("maibot-dashboard"); + if ( + dashboardVersions.dashboardLatestPypi + || dashboardVersions.dashboardLatestStablePypi + || dashboardVersions.dashboardLatestPrereleasePypi + ) { + Object.assign(versions, dashboardVersions); + versions.dashboardPypiSource = "PyPI"; + } + + return versions; + }; + + const readModuleVersions = async (): Promise => ({ + ...remoteModuleVersionsCache, + ...(await readLocalModuleVersions()), + }); + + const buildSnapshot = async (options: { refreshDependencies?: boolean } = {}): Promise => ({ + paths, + services: serviceManager.snapshot(), + serviceCommands: await serviceManager.getCommandConfigs(), + runtimePathConfigs: serviceManager.getRuntimePathConfigs(), + runtimeResourcePathConfigs: resourceLocationManager.getPathConfigs(), + terminalSettings: serviceManager.getTerminalSettings(), + appVersion: app.getVersion(), + moduleVersions: await readModuleVersions(), + platform: process.platform, + windowState: readWindowState(getMainWindow(), floatingMode, floatingEdgeSide), + initState: await initManager.getState({ refreshDependencies: options.refreshDependencies ?? false }), + startupAgreement: await initManager.getAgreementState(), + recentLogs: logStore.list(), + }); + + const broadcastSnapshot = async (): Promise => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return; + } + window.webContents.send("desktop:snapshot", await buildSnapshot()); + }; + + const scheduleRemoteModuleVersionsRefresh = (): void => { + if (remoteModuleVersionsRefreshPromise) { + return; + } + remoteModuleVersionsRefreshPromise = readRemoteModuleVersions() + .then(async (versions) => { + remoteModuleVersionsCache = versions; + await broadcastSnapshot(); + }) + .catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + logStore.append("desktop", "system", `读取远端版本失败: ${message}`); + }) + .finally(() => { + remoteModuleVersionsRefreshPromise = null; + }); + }; + + const scheduleInitDependencyRefresh = (): void => { + if (initDependencyRefreshPromise) { + return; + } + initDependencyRefreshPromise = initManager.refreshDependencyChecks() + .then(async () => { + await broadcastSnapshot(); + }) + .catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + logStore.append("desktop", "system", `环境依赖检查失败: ${message}`); + }) + .finally(() => { + initDependencyRefreshPromise = null; + }); + }; + + const createMaibotPluginClient = (): MaiBotPluginClient => + new MaiBotPluginClient({ + maibotRoot: paths.maibotRoot, + gitPath: initManager.getGitPath(), + getModuleSourceConfig: () => moduleUpdater.getSourceConfig(), + }); + let maibotPluginClient = createMaibotPluginClient(); + const localChatAdapter = new LocalChatAdapter(paths); + + const assertServicesStoppedForResourceMove = (): void => { + const active = serviceManager + .snapshot() + .filter( + (service) => + service.managed || + service.status === "starting" || + service.status === "running" || + service.status === "stopping", + ); + if (active.length > 0) { + throw new Error(`请先停止服务,再调整覆盖路径组: ${active.map((service) => service.name).join(", ")}`); + } + }; + + const applyResourceMigrationResult = async ( + result: RuntimeResourcePathChangeResult, + ): Promise => { + initManager.clearDependencyCache(); + serviceManager.reloadRuntimePaths(); + maibotPluginClient = createMaibotPluginClient(); + logStore.append( + "desktop", + "system", + `运行时资源路径已更新: ${result.previousPath} -> ${result.path}`, + ); + await broadcastSnapshot(); + return result; + }; + + const resetLauncherStores = async (): Promise => { + for (const service of serviceManager.snapshot()) { + await serviceManager.resetCommandConfig(service.id); + } + await serviceManager.resetRuntimePathConfig("python"); + await serviceManager.resetRuntimePathConfig("git"); + await serviceManager.saveTerminalSettings({ ...serviceManager.getTerminalSettings(), useEmbeddedTerminal: true }); + + for (const key of ["maibot", "napcat"] as const) { + const config = resourceLocationManager.getPathConfigs().find((item) => item.key === key); + if (config?.customized) { + await resourceLocationManager.resetPath(key); + } + } + + initManager.clearDependencyCache(); + serviceManager.reloadRuntimePaths(); + maibotPluginClient = createMaibotPluginClient(); + }; + + const resetLauncherSettings = async (): Promise => { + assertServicesStoppedForResourceMove(); + await resetLauncherStores(); + + const removedEntries: string[] = []; + for (const fileName of LAUNCHER_SETTING_FILES) { + removedEntries.push(...(await removeExistingPath(join(paths.userDataRoot, fileName)))); + } + + await mkdir(paths.userDataRoot, { recursive: true }); + await mkdir(paths.logsRoot, { recursive: true }); + logStore.append("desktop", "system", "启动器设置已清空,将重新进入启动引导"); + await broadcastSnapshot(); + return { + mode: "settings", + root: paths.userDataRoot, + removedEntries, + resetAt: Date.now(), + }; + }; + + const resetLauncherAll = async (): Promise => { + assertServicesStoppedForResourceMove(); + const root = paths.defaultResourceRoot; + if (samePath(root, paths.installRoot) || isPathInside(root, paths.installRoot)) { + throw new Error("当前运行时资源目录指向安装/开发目录,已阻止完整清空。请在打包版的独立运行时目录中执行。"); + } + if (!samePath(root, paths.userDataRoot) && !isPathInside(paths.userDataRoot, root)) { + throw new Error("当前运行时资源目录不在启动器数据目录内,已阻止完整清空。"); + } + + await resetLauncherStores(); + const resetEntries = samePath(root, paths.userDataRoot) + ? [...LAUNCHER_RUNTIME_DIRECTORIES, ...LAUNCHER_SETTING_FILES] + : undefined; + const removedEntries = await clearDirectoryContents(root, resetEntries); + await mkdir(paths.logsRoot, { recursive: true }); + initManager.clearDependencyCache(); + serviceManager.reloadRuntimePaths(); + maibotPluginClient = createMaibotPluginClient(); + logStore.append("desktop", "system", `运行时资源目录已清空: ${root}`); + await broadcastSnapshot(); + return { + mode: "all", + root, + removedEntries, + resetAt: Date.now(), + }; + }; + + serviceManager.on("snapshot", (services: ServiceDescriptor[]) => { + const window = getMainWindow(); + window?.webContents.send("services:snapshot", services); + void broadcastSnapshot(); + }); + logStore.onEntry((entry) => { + const window = getMainWindow(); + window?.webContents.send("logs:entry", entry); + }); + localChatAdapter.onEvent((event) => { + const window = getMainWindow(); + window?.webContents.send("localChat:event", event); + }); + + ipcMain.handle("desktop:getSnapshot", async (): Promise => { + await serviceManager.refresh(); + const snapshot = await buildSnapshot(); + scheduleInitDependencyRefresh(); + scheduleRemoteModuleVersionsRefresh(); + return snapshot; + }); + + ipcMain.handle("desktop:openExternal", async (_event, url: string): Promise => { + await shell.openExternal(url); + }); + + ipcMain.handle("desktop:openPath", async (_event, path: string): Promise => { + await shell.openPath(path); + }); + + ipcMain.handle("init:getState", async (): Promise => { + return initManager.getState({ refreshDependencies: true }); + }); + + ipcMain.handle("init:repair", async (): Promise => { + const result = await initManager.repair(); + logStore.append("desktop", "system", `初始化准备完成,变更 ${result.changedFiles.length} 个文件`); + await broadcastSnapshot(); + return result; + }); + + ipcMain.handle("init:resetSnowLuma", async (): Promise => { + await serviceManager.refresh(); + if (serviceManager.snapshot().some(isRuntimeBusy)) { + throw new Error("请先停止 MaiBot Core 和 QQ 后端,再重置 SnowLuma 组件。"); + } + + const result = await initManager.resetSnowLumaComponent(); + serviceManager.reloadRuntimePaths(); + logStore.append("desktop", "system", `SnowLuma 组件已重置: ${result.snowlumaRoot}`); + await broadcastSnapshot(); + return result; + }); + + ipcMain.handle("init:setQqBackend", async (_event, backend: QqBackend): Promise => { + const currentInitState = await initManager.getState(); + if (backend !== "napcat" && backend !== "snowluma") { + throw new Error("未知 QQ 后端"); + } + if (backend !== currentInitState.qqBackend && serviceManager.snapshot().some(isRuntimeBusy)) { + throw new Error("MaiBot Core 或 QQ 后端正在运行时不能切换 NapCat / SnowLuma,请先停止全部服务。"); + } + await initManager.setQqBackend(backend); + serviceManager.reloadRuntimePaths(); + const state = await initManager.getState(); + logStore.append("desktop", "system", `QQ 后端已切换为: ${backend === "snowluma" ? "SnowLuma" : "NapCat"}`); + await broadcastSnapshot(); + return state; + }); + + ipcMain.handle( + "init:setQqAccount", + async (_event, request: QqAccountSetupRequest): Promise => { + const currentInitState = await initManager.getState(); + const requestedBackend = request.qqBackend ?? currentInitState.qqBackend; + if ( + requestedBackend !== currentInitState.qqBackend && + serviceManager.snapshot().some(isRuntimeBusy) + ) { + throw new Error("MaiBot Core 或 QQ 后端正在运行时不能切换 NapCat / SnowLuma,请先停止全部服务。"); + } + const state = await initManager.setQqAccount( + request.qqAccount, + request.websocketToken, + request.chat, + request.qqBackend, + ); + serviceManager.reloadRuntimePaths(); + logStore.append("desktop", "system", `机器人 QQ 号已配置: ${request.qqAccount}`); + await broadcastSnapshot(); + return state; + }, + ); + + ipcMain.handle("agreements:getState", async (): Promise => { + return initManager.getAgreementState(); + }); + + ipcMain.handle("agreements:confirm", async (): Promise => { + const result = await initManager.confirmAgreements(); + logStore.append("desktop", "system", `MaiBot EULA 与隐私政策已确认,写入 ${result.changedFiles.length} 个文件`); + await broadcastSnapshot(); + return result; + }); + + ipcMain.handle("modules:updateMaibot", async (_event, tag?: string): Promise => { + const maibot = serviceManager.snapshot().find((service) => service.id === "maibot"); + if (maibot?.managed || maibot?.status === "starting" || maibot?.status === "running" || maibot?.status === "stopping") { + throw new Error("请先停止 MaiBot Core,再更新 MaiBot 模块。"); + } + + logStore.append("desktop", "system", "开始更新 MaiBot 模块:使用可用 Git 强制拉取远端代码"); + const result = await moduleUpdater.updateMaiBot(tag); + logStore.append( + "desktop", + "system", + `MaiBot 模块更新完成: ${result.before ?? "-"} -> ${result.after ?? "-"} (${result.changed ? "已更新" : "已是最新"})`, + ); + await broadcastSnapshot(); + return result; + }); + + ipcMain.handle("modules:listMaibotTags", async (): Promise => { + return moduleUpdater.listMaiBotTags(); + }); + + ipcMain.handle("modules:getSourceConfig", async (): Promise => { + return moduleUpdater.getSourceConfig(); + }); + + ipcMain.handle("modules:saveSourceConfig", async (_event, config: ModuleSourceUpdate): Promise => { + const result = await moduleUpdater.saveSourceConfig(config); + logStore.append("desktop", "system", `模块更新源已切换: ${result.preset} (${result.maibotUrl})`); + return result; + }); + + + ipcMain.handle("data:importMaibotDb", async (): Promise => { + const maibot = serviceManager.snapshot().find((service) => service.id === "maibot"); + if (maibot?.managed || maibot?.status === "starting" || maibot?.status === "running" || maibot?.status === "stopping") { + throw new Error("请先停止 MaiBot Core,再导入旧版本数据库。"); + } + + const mainWindow = getMainWindow(); + const dialogOptions: Electron.OpenDialogOptions = { + title: "选择旧版本 MaiBot.db", + properties: ["openFile"], + filters: [ + { name: "MaiBot 数据库", extensions: ["db"] }, + { name: "全部文件", extensions: ["*"] }, + ], + }; + const result = mainWindow + ? await dialog.showOpenDialog(mainWindow, dialogOptions) + : await dialog.showOpenDialog(dialogOptions); + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + const importResult = await initManager.importMaiBotDatabase(result.filePaths[0]); + logStore.append( + "desktop", + "system", + `MaiBot.db 导入完成: ${importResult.sourcePath} -> ${importResult.destPath}`, + ); + await broadcastSnapshot(); + return importResult; + }); + + ipcMain.handle( + "data:importMaibotConfig", + async (_event, fileName: MaiBotConfigFileName): Promise => { + const maibot = serviceManager.snapshot().find((service) => service.id === "maibot"); + if ( + maibot?.managed || + maibot?.status === "starting" || + maibot?.status === "running" || + maibot?.status === "stopping" + ) { + throw new Error("请先停止 MaiBot Core,再覆盖配置文件。"); + } + + if (fileName !== "bot_config.toml" && fileName !== "model_config.toml") { + throw new Error(`不支持的配置文件名: ${fileName}`); + } + + const mainWindow = getMainWindow(); + const dialogOptions: Electron.OpenDialogOptions = { + title: `选择 ${fileName}`, + properties: ["openFile"], + filters: [ + { name: "TOML 配置", extensions: ["toml"] }, + { name: "全部文件", extensions: ["*"] }, + ], + }; + const result = mainWindow + ? await dialog.showOpenDialog(mainWindow, dialogOptions) + : await dialog.showOpenDialog(dialogOptions); + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + const importResult = await initManager.importMaiBotConfig(fileName, result.filePaths[0]); + logStore.append( + "desktop", + "system", + `MaiBot ${fileName} 导入完成: ${importResult.sourcePath} -> ${importResult.destPath}`, + ); + await broadcastSnapshot(); + return importResult; + }, + ); + + ipcMain.handle("data:resetMaibotData", async (): Promise => { + const maibot = serviceManager.snapshot().find((service) => service.id === "maibot"); + if (maibot?.managed || maibot?.status === "starting" || maibot?.status === "running" || maibot?.status === "stopping") { + throw new Error("请先停止 MaiBot Core,再重置数据。"); + } + + const resetResult = await initManager.resetMaiBotData(); + logStore.append( + "desktop", + "system", + `已清空 MaiBot data 目录 (${resetResult.removedEntries.length} 项): ${resetResult.dataDir}`, + ); + await broadcastSnapshot(); + return resetResult; + }); + + ipcMain.handle("launcher:resetSettings", async (): Promise => { + return resetLauncherSettings(); + }); + + ipcMain.handle("launcher:resetAll", async (): Promise => { + return resetLauncherAll(); + }); + + ipcMain.handle("plugins:listMarket", async ( + _event, + serviceUrl?: string, + options?: MaiBotPluginListOptions, + ): Promise => { + return maibotPluginClient.listMarket(serviceUrl, options); + }); + + ipcMain.handle("plugins:listInstalled", async (_event, serviceUrl?: string): Promise => { + return maibotPluginClient.listInstalled(serviceUrl); + }); + + ipcMain.handle( + "plugins:install", + async (_event, request: MaiBotPluginOperationRequest): Promise => { + if (!request.pluginId || !request.repositoryUrl) { + throw new Error("Plugin id and repository url are required."); + } + const result = await maibotPluginClient.install(request.pluginId, request.repositoryUrl, request.branch); + logStore.append("desktop", "system", `MaiBot plugin installed: ${request.pluginId}`); + return result; + }, + ); + + ipcMain.handle( + "plugins:update", + async (_event, request: MaiBotPluginOperationRequest): Promise => { + if (!request.pluginId || !request.repositoryUrl) { + throw new Error("Plugin id and repository url are required."); + } + const result = await maibotPluginClient.update( + request.pluginId, + request.repositoryUrl, + request.branch, + request.latestVersion, + ); + logStore.append("desktop", "system", `MaiBot plugin updated: ${request.pluginId}`); + return result; + }, + ); + + ipcMain.handle( + "plugins:uninstall", + async (_event, pluginId: string): Promise => { + if (!pluginId) { + throw new Error("Plugin id is required."); + } + const result = await maibotPluginClient.uninstall(pluginId); + logStore.append("desktop", "system", `MaiBot plugin uninstalled: ${pluginId}`); + return result; + }, + ); + + ipcMain.handle("plugins:getConfig", async (_event, pluginId: string, serviceUrl?: string): Promise => { + return maibotPluginClient.getConfig(pluginId, serviceUrl); + }); + + ipcMain.handle( + "plugins:saveConfig", + async ( + _event, + pluginId: string, + config: Record, + serviceUrl?: string, + ): Promise => { + const result = await maibotPluginClient.saveConfig(pluginId, config, serviceUrl); + logStore.append("desktop", "system", `MaiBot plugin config saved: ${pluginId}`); + await broadcastSnapshot(); + return result; + }, + ); + + ipcMain.handle("plugins:getReadme", async (_event, pluginId: string, repositoryUrl?: string): Promise => { + return maibotPluginClient.getReadme(pluginId, repositoryUrl); + }); + + ipcMain.handle("plugins:getStats", async (_event, pluginId: string): Promise => { + return maibotPluginClient.getStats(pluginId); + }); + + ipcMain.handle("statistics:getMaibot", async (): Promise => { + return readMaiBotStatistics(paths); + }); + + ipcMain.handle("pythonDeps:getState", (): PythonOverridesState => { + return pythonDependencyManager.getState(); + }); + + ipcMain.handle("pythonDeps:saveSourcePreset", async (_event, preset: PythonPackageSourcePreset): Promise => { + const state = await pythonDependencyManager.saveSourcePreset(preset); + await broadcastSnapshot(); + return state; + }); + + ipcMain.handle("pythonDeps:listVersions", async (_event, packageName: ManagedPythonPackageName): Promise => { + return pythonDependencyManager.listVersions(packageName); + }); + + ipcMain.handle("pythonDeps:installVersion", async (_event, request: PythonPackageInstallRequest): Promise => { + const maibot = serviceManager.snapshot().find((service) => service.id === "maibot"); + if (maibot?.managed || maibot?.status === "starting" || maibot?.status === "running" || maibot?.status === "stopping") { + throw new Error("请先停止 MaiBot Core,再更新 Python 依赖。"); + } + + logStore.append("desktop", "system", `开始更新 Python 覆盖依赖: ${request.packageName}==${request.version}`); + const result = await pythonDependencyManager.installVersion(request); + logStore.append( + "desktop", + "system", + `Python 覆盖依赖更新完成: ${result.packageName}==${result.version} -> ${result.targetDir}`, + ); + await broadcastSnapshot(); + return result; + }); + + ipcMain.handle("services:start", async (_event, serviceId: ServiceId): Promise => { + const descriptor = await serviceManager.start(serviceId); + await broadcastSnapshot(); + return descriptor; + }); + + ipcMain.handle("services:stop", async (_event, serviceId: ServiceId): Promise => { + const descriptor = await serviceManager.stop(serviceId); + await broadcastSnapshot(); + return descriptor; + }); + + ipcMain.handle("services:restart", async (_event, serviceId: ServiceId): Promise => { + const descriptor = await serviceManager.restart(serviceId); + await broadcastSnapshot(); + return descriptor; + }); + + ipcMain.handle("services:startAll", async (): Promise => { + const services = await serviceManager.startAll(); + await broadcastSnapshot(); + return services; + }); + + ipcMain.handle("services:stopAll", async (): Promise => { + const services = await serviceManager.stopAll(); + await broadcastSnapshot(); + return services; + }); + + ipcMain.handle("services:refresh", async (): Promise => { + const services = await serviceManager.refresh(); + await broadcastSnapshot(); + return services; + }); + + ipcMain.handle("services:saveCommandConfig", async (_event, config: ServiceCommandUpdate): Promise => { + const configs = await serviceManager.saveCommandConfig(config); + await broadcastSnapshot(); + return configs; + }); + + ipcMain.handle("services:resetCommandConfig", async (_event, serviceId: ServiceId): Promise => { + const configs = await serviceManager.resetCommandConfig(serviceId); + await broadcastSnapshot(); + return configs; + }); + + ipcMain.handle("services:saveRuntimePathConfig", async (_event, config: RuntimePathUpdate): Promise => { + const configs = await serviceManager.saveRuntimePathConfig(config); + await broadcastSnapshot(); + return configs; + }); + + ipcMain.handle("services:resetRuntimePathConfig", async (_event, key: RuntimePathKey): Promise => { + const configs = await serviceManager.resetRuntimePathConfig(key); + await broadcastSnapshot(); + return configs; + }); + + ipcMain.handle("services:listPythonRuntimeCandidates", async (): Promise => { + return initManager.listSystemPythonRuntimeCandidates(); + }); + + ipcMain.handle("services:selectPythonRuntimePath", async (): Promise => { + const mainWindow = getMainWindow(); + const dialogOptions: Electron.OpenDialogOptions = { + title: "选择 Python 可执行文件", + properties: ["openFile"], + filters: [ + { name: "Python", extensions: process.platform === "win32" ? ["exe"] : ["*"] }, + { name: "全部文件", extensions: ["*"] }, + ], + }; + const result = mainWindow + ? await dialog.showOpenDialog(mainWindow, dialogOptions) + : await dialog.showOpenDialog(dialogOptions); + return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0]; + }); + + ipcMain.handle("services:saveTerminalSettings", async (_event, settings: TerminalSettings): Promise => { + const config = await serviceManager.saveTerminalSettings(settings); + await broadcastSnapshot(); + return config; + }); + + const chooseResourcePath = async (title: string): Promise => { + const mainWindow = getMainWindow(); + const dialogOptions: Electron.OpenDialogOptions = { + title, + properties: ["openDirectory", "createDirectory"], + }; + const result = mainWindow + ? await dialog.showOpenDialog(mainWindow, dialogOptions) + : await dialog.showOpenDialog(dialogOptions); + return result.canceled || result.filePaths.length === 0 ? undefined : result.filePaths[0]; + }; + + ipcMain.handle( + "resources:migratePath", + async (_event, key: RuntimeResourcePathKey): Promise => { + assertServicesStoppedForResourceMove(); + const targetPath = await chooseResourcePath("选择迁移目标目录"); + if (!targetPath) { + return null; + } + + const migration = await resourceLocationManager.migratePath(key, targetPath); + return applyResourceMigrationResult(migration); + }, + ); + + ipcMain.handle( + "resources:selectPath", + async (_event, key: RuntimeResourcePathKey): Promise => { + assertServicesStoppedForResourceMove(); + const targetPath = await chooseResourcePath("选择已有目录"); + if (!targetPath) { + return null; + } + + const selection = await resourceLocationManager.selectPath(key, targetPath); + return applyResourceMigrationResult(selection); + }, + ); + + ipcMain.handle( + "resources:savePath", + async (_event, key: RuntimeResourcePathKey, targetPath: string): Promise => { + assertServicesStoppedForResourceMove(); + const selection = await resourceLocationManager.selectPath(key, targetPath); + return applyResourceMigrationResult(selection); + }, + ); + + ipcMain.handle("resources:resetPath", async (_event, key: RuntimeResourcePathKey): Promise => { + assertServicesStoppedForResourceMove(); + const migration = await resourceLocationManager.resetPath(key); + return applyResourceMigrationResult(migration); + }); + + ipcMain.handle("logs:list", (): LogEntry[] => logStore.list()); + + ipcMain.handle("logs:clear", (): void => { + logStore.clear(); + void broadcastSnapshot(); + }); + + ipcMain.handle("localChat:connect", async (_event, request?: LocalChatConnectRequest): Promise => { + return localChatAdapter.connect(request); + }); + + ipcMain.handle("localChat:disconnect", async (): Promise => { + localChatAdapter.disconnect(); + }); + + ipcMain.handle("localChat:send", async (_event, request: LocalChatSendRequest): Promise => { + return localChatAdapter.send(request); + }); + + ipcMain.handle("localChat:listMessages", async (): Promise => { + return localChatAdapter.listMessages(); + }); + + ipcMain.handle("desktop:openLogsDirectory", async (): Promise => { + await mkdir(paths.logsRoot, { recursive: true }); + await shell.openPath(paths.logsRoot); + }); + + ipcMain.handle("desktop:chooseCloseAction", async (_event, action: CloseAction): Promise => { + const mainWindow = getMainWindow(); + + if (action === "minimize") { + mainWindow?.hide(); + return; + } + + requestQuit(); + }); + + ipcMain.handle("desktop:show", (): void => showMainWindow()); + + ipcMain.handle("desktop:window:minimize", (): void => { + getMainWindow()?.minimize(); + }); + + ipcMain.handle("desktop:window:toggleMaximize", (): void => { + const window = getMainWindow(); + if (!window) return; + if (window.isMaximized()) { + window.unmaximize(); + } else { + window.maximize(); + } + }); + + ipcMain.handle("desktop:window:close", (): void => { + getMainWindow()?.close(); + }); + + ipcMain.handle("desktop:window:setFloatingMode", (_event, enabled: boolean): WindowState => applyFloatingMode(enabled)); + + ipcMain.handle("desktop:window:setFloatingPanelExpanded", (_event, expanded: boolean): WindowState => + applyFloatingPanelExpanded(expanded), + ); + + ipcMain.handle("desktop:window:moveFloatingBy", (_event, deltaX: number, deltaY: number): WindowState => + moveFloatingBy(deltaX, deltaY), + ); + ipcMain.handle( + "desktop:window:moveFloatingTo", + (_event, screenX: number, screenY: number, offsetX: number, offsetY: number): WindowState => + moveFloatingTo(screenX, screenY, offsetX, offsetY), + ); + + ipcMain.handle("desktop:window:finishFloatingDrag", (): WindowState => finishFloatingDrag()); + + ipcMain.handle("desktop:window:getState", (): WindowState => + readWindowState(getMainWindow(), floatingMode, floatingEdgeSide), + ); + + return { + localChatAdapter, + dispose: () => { + localChatAdapter.dispose(); + }, + }; +} diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts new file mode 100644 index 0000000..c05f9b1 --- /dev/null +++ b/src/main/ipc/pty.ts @@ -0,0 +1,80 @@ +import type { BrowserWindow } from "electron"; +import { ipcMain } from "electron"; +import type { + PtyInputRequest, + PtyResizeRequest, + PtySessionSnapshot, + PtyStartRequest, + PtyStopRequest, +} from "../../shared/contracts"; +import { PtySessionManager } from "../pty/pty-session-manager"; + +interface RegisterPtyIpcOptions { + manager: PtySessionManager; + getMainWindow: () => BrowserWindow | null; +} + +function isMissingSession(error: unknown): boolean { + return error instanceof Error && error.message.startsWith("PTY session not found:"); +} + +export function registerPtyIpc({ manager, getMainWindow }: RegisterPtyIpcOptions): void { + const sendToRenderer = (channel: string, payload: unknown): void => { + const window = getMainWindow(); + if (!window || window.isDestroyed()) { + return; + } + + window.webContents.send(channel, payload); + }; + + manager.on("data", (event) => sendToRenderer("pty:data", event)); + manager.on("exit", (event) => sendToRenderer("pty:exit", event)); + manager.on("error", (event) => sendToRenderer("pty:error", event)); + manager.on("snapshot", (snapshot) => sendToRenderer("pty:snapshot", snapshot)); + + ipcMain.handle("pty:start", (_event, request: PtyStartRequest): PtySessionSnapshot => { + return manager.start(request); + }); + + ipcMain.handle("pty:input", (_event, request: PtyInputRequest): void => { + manager.input(request); + }); + + ipcMain.handle("pty:resize", (_event, request: PtyResizeRequest): void => { + try { + manager.resize(request); + } catch (error) { + if (!isMissingSession(error)) { + throw error; + } + } + }); + + ipcMain.handle("pty:stop", (_event, request: PtyStopRequest): void => { + manager.stop(request); + }); + + ipcMain.handle("pty:kill", (_event, sessionId: string): void => { + manager.kill(sessionId); + }); + + ipcMain.handle("pty:clear", (_event, sessionId: string): void => { + manager.clear(sessionId); + }); + + ipcMain.handle("pty:list", (): PtySessionSnapshot[] => { + return manager.list(); + }); + + ipcMain.handle("pty:getBuffer", (_event, sessionId: string): string => { + try { + return manager.getBuffer(sessionId); + } catch (error) { + if (!isMissingSession(error)) { + throw error; + } + return ""; + } + }); +} diff --git a/src/main/pty/encoding.ts b/src/main/pty/encoding.ts new file mode 100644 index 0000000..0079140 --- /dev/null +++ b/src/main/pty/encoding.ts @@ -0,0 +1,52 @@ +import iconv from "iconv-lite"; +import type { PtyEncoding } from "../../shared/contracts"; + +const codePages: Record = { + auto: "65001", + utf8: "65001", + gbk: "936", + gb18030: "54936", + big5: "950", + shiftjis: "932", + euckr: "949", + utf16le: "1200", +}; + +const iconvEncodings: Partial> = { + gbk: "gbk", + gb18030: "gb18030", + big5: "big5", + shiftjis: "shift_jis", + euckr: "euc-kr", + utf16le: "utf16le", +}; + +export function getWindowsCodePage(encoding: PtyEncoding): string { + return codePages[encoding] ?? codePages.utf8; +} + +export function getNodePtyEncoding(encoding: PtyEncoding): string | null { + return encoding === "auto" || encoding === "utf8" ? "utf8" : null; +} + +export function decodePtyData(data: string | Buffer, encoding: PtyEncoding): string { + if (typeof data === "string") { + return data; + } + + const iconvEncoding = iconvEncodings[encoding]; + if (!iconvEncoding) { + return data.toString("utf8"); + } + + return iconv.decode(data, iconvEncoding); +} + +export function encodePtyInput(data: string, encoding: PtyEncoding): string | Buffer { + const iconvEncoding = iconvEncodings[encoding]; + if (!iconvEncoding) { + return data; + } + + return iconv.encode(data, iconvEncoding); +} diff --git a/src/main/pty/pty-session-manager.ts b/src/main/pty/pty-session-manager.ts new file mode 100644 index 0000000..3dd04e6 --- /dev/null +++ b/src/main/pty/pty-session-manager.ts @@ -0,0 +1,524 @@ +import { EventEmitter } from "node:events"; +import { spawn as spawnChild } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { chmodSync, existsSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import * as nodePty from "node-pty"; +import type { + PtyDataEvent, + PtyEncoding, + PtyErrorEvent, + PtyExitEvent, + PtyResizeRequest, + PtySessionSnapshot, + PtyStartRequest, + PtyStopRequest, +} from "../../shared/contracts"; +import { decodePtyData, encodePtyInput, getNodePtyEncoding, getWindowsCodePage } from "./encoding"; + +const MIN_COLS = 5; +const MIN_ROWS = 5; +const DEFAULT_COLS = 100; +const DEFAULT_ROWS = 32; +const DEFAULT_FORCE_AFTER_MS = 10_000; +const BUFFER_LIMIT = 5_000_000; +const require = createRequire(import.meta.url); + +type PtySessionEventMap = { + data: [PtyDataEvent]; + exit: [PtyExitEvent]; + error: [PtyErrorEvent]; + snapshot: [PtySessionSnapshot]; +}; + +function clampDimension(value: number | undefined, fallback: number): number { + if (!Number.isFinite(value) || !value) { + return fallback; + } + + return Math.max(Math.floor(value), value === fallback ? fallback : MIN_COLS); +} + +function normalizeRows(value: number | undefined): number { + if (!Number.isFinite(value) || !value) { + return DEFAULT_ROWS; + } + + return Math.max(Math.floor(value), MIN_ROWS); +} + +function normalizeEncoding(encoding: PtyEncoding | undefined): PtyEncoding { + return encoding ?? "auto"; +} + +function resolveDefaultCwd(cwd: string | undefined): string { + if (!cwd) { + return process.cwd(); + } + + const absolute = resolve(cwd); + try { + return statSync(absolute).isDirectory() ? absolute : process.cwd(); + } catch { + return process.cwd(); + } +} + +interface ResolvedCommand { + file: string; + args: string[]; + displayCommand: string[]; + title: string; +} + +function resolveExistingExecutable(candidates: Array, fallback: string): string { + for (const candidate of candidates) { + const trimmed = candidate?.trim(); + if (!trimmed) { + continue; + } + + if (existsSync(trimmed)) { + return trimmed; + } + } + + return fallback; +} + +function resolveWindowsShell(requestedShell: string | undefined): string { + return resolveExistingExecutable([requestedShell, process.env.ComSpec], "cmd.exe"); +} + +function resolveUnixShell(requestedShell: string | undefined): string { + const fallback = existsSync("/bin/sh") ? "/bin/sh" : "sh"; + return resolveExistingExecutable( + [requestedShell, process.env.SHELL, "/bin/zsh", "/bin/bash", "/bin/sh"], + fallback, + ); +} + +function resolveCommand(request: PtyStartRequest, encoding: PtyEncoding): ResolvedCommand { + const requestedCommand = request.command?.filter(Boolean); + const codePage = getWindowsCodePage(encoding); + + if (request.commandLine) { + if (process.platform === "win32") { + return { + file: resolveWindowsShell(request.shell), + args: ["/D", "/S", "/C", `chcp ${codePage} > nul & ${request.commandLine}`], + displayCommand: [request.commandLine], + title: request.title ?? request.commandLine, + }; + } + + const shell = resolveUnixShell(request.shell); + return { + file: shell, + args: ["-lc", request.commandLine], + displayCommand: [request.commandLine], + title: request.title ?? request.commandLine, + }; + } + + if (requestedCommand && requestedCommand.length > 0) { + if (process.platform === "win32") { + return { + file: requestedCommand[0], + args: requestedCommand.slice(1), + displayCommand: requestedCommand, + title: request.title ?? basename(requestedCommand[0]), + }; + } + + return { + file: requestedCommand[0], + args: requestedCommand.slice(1), + displayCommand: requestedCommand, + title: request.title ?? basename(requestedCommand[0]), + }; + } + + if (process.platform === "win32") { + const shell = resolveWindowsShell(request.shell); + return { + file: shell, + args: ["/K", `chcp ${codePage} > nul`], + displayCommand: [shell], + title: request.title ?? "Windows Shell", + }; + } + + const shell = resolveUnixShell(request.shell); + return { + file: shell, + args: process.platform === "darwin" ? ["-l"] : [], + displayCommand: [shell], + title: request.title ?? basename(shell), + }; +} + +function createEnvironment(extraEnv: Record | undefined): Record { + const source = { + ...process.env, + TERM: process.platform === "win32" ? "xterm-256color" : (process.env.TERM ?? "xterm-256color"), + COLORTERM: "truecolor", + FORCE_COLOR: "1", + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + ...extraEnv, + }; + const env: Record = {}; + + for (const [key, value] of Object.entries(source)) { + if (value !== undefined) { + env[key] = value; + } + } + + return env; +} + +function toUnpackedAsarPath(path: string): string { + return path.replace("app.asar", "app.asar.unpacked").replace("node_modules.asar", "node_modules.asar.unpacked"); +} + +function ensureExecutable(path: string): void { + const stat = statSync(path); + if ((stat.mode & 0o111) !== 0) { + return; + } + + chmodSync(path, stat.mode | 0o755); +} + +function ensureNodePtySpawnHelperExecutable(): void { + if (process.platform === "win32") { + return; + } + + let nodePtyPackagePath: string; + try { + nodePtyPackagePath = require.resolve("node-pty/package.json"); + } catch { + return; + } + + const packageRoot = dirname(nodePtyPackagePath); + const helperPath = toUnpackedAsarPath( + join(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"), + ); + if (existsSync(helperPath)) { + ensureExecutable(helperPath); + } +} + +class PtySession { + private ptyProcess: nodePty.IPty | null = null; + private outputBuffer = ""; + private stopTimer: NodeJS.Timeout | null = null; + private readonly createdAt = Date.now(); + private snapshot: PtySessionSnapshot; + + constructor( + private readonly request: PtyStartRequest, + private readonly emitEvent: ( + event: K, + ...args: PtySessionEventMap[K] + ) => void, + ) { + const encoding = normalizeEncoding(request.encoding); + const cwd = resolveDefaultCwd(request.cwd); + const command = resolveCommand(request, encoding); + + this.snapshot = { + id: request.id ?? randomUUID(), + title: command.title, + cwd, + command: command.displayCommand, + cols: clampDimension(request.cols, DEFAULT_COLS), + rows: normalizeRows(request.rows), + encoding, + status: "starting", + startedAt: this.createdAt, + }; + } + + get id(): string { + return this.snapshot.id; + } + + get status(): PtySessionSnapshot["status"] { + return this.snapshot.status; + } + + get pid(): number | undefined { + return this.snapshot.pid; + } + + getBuffer(): string { + return this.outputBuffer; + } + + start(): PtySessionSnapshot { + const encoding = this.snapshot.encoding; + const command = resolveCommand(this.request, encoding); + + try { + ensureNodePtySpawnHelperExecutable(); + this.ptyProcess = nodePty.spawn(command.file, command.args, { + name: "xterm-256color", + cols: this.snapshot.cols, + rows: this.snapshot.rows, + cwd: this.snapshot.cwd, + env: createEnvironment(this.request.env), + encoding: getNodePtyEncoding(encoding), + handleFlowControl: true, + useConpty: true, + }); + } catch (error) { + this.markError(error); + throw error; + } + + this.snapshot = { + ...this.snapshot, + pid: this.ptyProcess.pid, + status: "running", + }; + this.emitSnapshot(); + + this.ptyProcess.onData((data) => { + const decoded = decodePtyData(data as string | Buffer, encoding); + this.appendOutput(decoded); + this.emitEvent("data", { + sessionId: this.id, + data: decoded, + }); + }); + + this.ptyProcess.onExit(({ exitCode, signal }) => { + this.clearStopTimer(); + this.snapshot = { + ...this.snapshot, + status: "exited", + exitCode, + signal, + endedAt: Date.now(), + }; + this.emitEvent("exit", { + sessionId: this.id, + exitCode, + signal, + }); + this.emitSnapshot(); + }); + + return this.getSnapshot(); + } + + write(data: string): void { + if (!this.ptyProcess || this.status !== "running") { + throw new Error("PTY session is not running"); + } + + this.ptyProcess.write(encodePtyInput(data, this.snapshot.encoding)); + } + + resize(request: Omit): void { + const cols = clampDimension(request.cols, this.snapshot.cols); + const rows = normalizeRows(request.rows); + + this.snapshot = { + ...this.snapshot, + cols, + rows, + }; + + if (this.ptyProcess && this.status === "running") { + this.ptyProcess.resize(cols, rows); + } + + this.emitSnapshot(); + } + + clear(): void { + this.outputBuffer = ""; + this.ptyProcess?.clear(); + } + + stop(forceAfterMs = DEFAULT_FORCE_AFTER_MS): void { + if (!this.ptyProcess || this.status === "exited") { + return; + } + + this.snapshot = { + ...this.snapshot, + status: "stopping", + }; + this.emitSnapshot(); + + try { + this.ptyProcess.write("\x03"); + } catch (error) { + this.emitError(error); + } + + this.clearStopTimer(); + this.stopTimer = setTimeout(() => { + if (this.status !== "exited") { + this.kill(); + } + }, forceAfterMs); + } + + kill(): void { + this.clearStopTimer(); + + if (!this.ptyProcess || this.status === "exited") { + return; + } + + if (process.platform === "win32" && this.pid) { + const killer = spawnChild("taskkill", ["/F", "/T", "/PID", String(this.pid)], { + windowsHide: true, + stdio: "ignore", + }); + killer.once("error", (error) => { + this.emitError(error); + this.ptyProcess?.kill(); + }); + return; + } + + this.ptyProcess.kill(); + } + + getSnapshot(): PtySessionSnapshot { + return { ...this.snapshot }; + } + + private appendOutput(data: string): void { + this.outputBuffer += data; + if (this.outputBuffer.length > BUFFER_LIMIT) { + this.outputBuffer = this.outputBuffer.slice(-BUFFER_LIMIT); + } + } + + private markError(error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + this.snapshot = { + ...this.snapshot, + status: "error", + error: message, + endedAt: Date.now(), + }; + this.emitEvent("error", { + sessionId: this.id, + message, + }); + this.emitSnapshot(); + } + + private emitError(error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + this.emitEvent("error", { + sessionId: this.id, + message, + }); + } + + private emitSnapshot(): void { + this.emitEvent("snapshot", this.getSnapshot()); + } + + private clearStopTimer(): void { + if (!this.stopTimer) { + return; + } + + clearTimeout(this.stopTimer); + this.stopTimer = null; + } +} + +export class PtySessionManager extends EventEmitter { + private readonly sessions = new Map(); + + start(request: PtyStartRequest): PtySessionSnapshot { + const session = new PtySession( + { + cols: DEFAULT_COLS, + rows: DEFAULT_ROWS, + cwd: process.platform === "win32" ? process.cwd() : homedir(), + ...request, + }, + (event, ...args) => { + this.emit(event, ...args); + }, + ); + + const existing = this.sessions.get(session.id); + if (existing?.status === "exited" || existing?.status === "error") { + this.sessions.delete(session.id); + } else if (existing) { + throw new Error(`PTY session already exists: ${session.id}`); + } + + this.sessions.set(session.id, session); + + try { + return session.start(); + } catch (error) { + this.sessions.delete(session.id); + throw error; + } + } + + input({ sessionId, data }: { sessionId: string; data: string }): void { + this.getRequired(sessionId).write(data); + } + + resize({ sessionId, cols, rows }: PtyResizeRequest): void { + this.getRequired(sessionId).resize({ cols, rows }); + } + + stop({ sessionId, forceAfterMs }: PtyStopRequest): void { + this.getRequired(sessionId).stop(forceAfterMs); + } + + kill(sessionId: string): void { + this.getRequired(sessionId).kill(); + } + + clear(sessionId: string): void { + this.getRequired(sessionId).clear(); + } + + list(): PtySessionSnapshot[] { + return [...this.sessions.values()].map((session) => session.getSnapshot()); + } + + getBuffer(sessionId: string): string { + return this.getRequired(sessionId).getBuffer(); + } + + dispose(): void { + for (const session of this.sessions.values()) { + session.kill(); + } + this.sessions.clear(); + this.removeAllListeners(); + } + + private getRequired(sessionId: string): PtySession { + const session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`PTY session not found: ${sessionId}`); + } + + return session; + } +} diff --git a/src/main/services/init-manager.ts b/src/main/services/init-manager.ts new file mode 100644 index 0000000..919454b --- /dev/null +++ b/src/main/services/init-manager.ts @@ -0,0 +1,2424 @@ +import { execFile } from "node:child_process"; +import { createHash, randomBytes } from "node:crypto"; +import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { delimiter, dirname, join, relative, resolve, sep } from "node:path"; +import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; +import type { + AgreementDocument, + AgreementDocumentId, + InitCheck, + InitRepairResult, + InitState, + MaiBotConfigFileName, + MaiBotConfigImportResult, + MaiBotDataImportResult, + MaiBotDataResetResult, + NapcatAdapterChatConfig, + NapcatAdapterConfig, + NapcatChatListMode, + QqBackend, + RuntimePaths, + PythonRuntimeCandidate, + ServiceId, + SnowLumaResetResult, + StartupAgreementConfirmResult, + StartupAgreementState, +} from "../../shared/contracts"; + +const QQ_PATTERN = /qq_account\s*=\s*["']?(\d+)["']?/; +const DEPENDENCY_CACHE_MS = 15_000; +const PYTHON_RUNTIME_DIR = "python"; +const GIT_RUNTIME_DIR = "git"; +const PYTHON_MINIMUM_VERSION = "3.12"; +const PYTHON_DOWNLOAD_URL = "https://www.python.org/downloads/windows/"; +const GIT_DOWNLOAD_URL = "https://git-scm.com/download/win"; +const NAPCAT_FALLBACK_VERSION = "9.9.26-44498"; +const MAIBOT_FALLBACK_CONFIG_VERSION = "8.10.22"; +const QQ_BACKEND_FILE = "qq-backend.json"; +const MESSAGE_PLATFORM_FILE = "message-platform.json"; +const PYTHON_OVERRIDES_IGNORED_ENTRIES = new Set([".keep", "resource.lock"]); + +function uniqueExistingPaths(paths: string[]): string[] { + const seen = new Set(); + const existing: string[] = []; + + for (const path of paths) { + const normalized = process.platform === "win32" ? path.toLowerCase() : path; + if (seen.has(normalized) || !existsSync(path)) { + continue; + } + seen.add(normalized); + existing.push(path); + } + + return existing; +} + +function uniquePythonCandidates(candidates: PythonRuntimeCandidate[]): PythonRuntimeCandidate[] { + const seen = new Set(); + const unique: PythonRuntimeCandidate[] = []; + + for (const candidate of candidates) { + if (isWindowsAppsPythonAlias(candidate.path)) { + continue; + } + const normalized = normalizePathForCompare(candidate.path); + if (seen.has(normalized) || !existsSync(candidate.path)) { + continue; + } + seen.add(normalized); + unique.push(candidate); + } + + return unique; +} + +function normalizePathForCompare(path: string): string { + const resolved = resolve(path); + return process.platform === "win32" ? resolved.toLowerCase() : resolved; +} + +function samePath(left: string, right: string): boolean { + return normalizePathForCompare(left) === normalizePathForCompare(right); +} + +function sameOrInsidePath(parent: string, child: string): boolean { + if (samePath(parent, child)) { + return true; + } + const diff = relative(resolve(parent), resolve(child)); + return Boolean(diff) && diff !== ".." && !diff.startsWith(`..${sep}`); +} + +function cleanPathEntry(entry: string): string { + return entry.trim().replace(/^"|"$/gu, ""); +} + +function isWindowsAppsAlias(path: string): boolean { + return process.platform === "win32" && /\\microsoft\\windowsapps\\git\.exe$/iu.test(path); +} + +function isWindowsAppsPythonAlias(path: string): boolean { + return process.platform === "win32" && /\\microsoft\\windowsapps\\python(?:3)?\.exe$/iu.test(path); +} + +function pathGitCandidates(): string[] { + const names = process.platform === "win32" ? ["git.exe"] : ["git"]; + const pathEntries = (process.env.PATH ?? "") + .split(delimiter) + .map(cleanPathEntry) + .filter(Boolean); + const candidates: string[] = []; + + for (const entry of pathEntries) { + for (const name of names) { + const candidate = join(entry, name); + if (!isWindowsAppsAlias(candidate)) { + candidates.push(candidate); + } + } + } + + return candidates; +} + +function pathPythonCandidates(): string[] { + const names = process.platform === "win32" ? ["python.exe", "python3.exe"] : ["python3", "python"]; + const pathEntries = (process.env.PATH ?? "") + .split(delimiter) + .map(cleanPathEntry) + .filter(Boolean); + const candidates: string[] = []; + + for (const entry of pathEntries) { + for (const name of names) { + const candidate = join(entry, name); + if (!isWindowsAppsPythonAlias(candidate)) { + candidates.push(candidate); + } + } + } + + return candidates; +} + +function childPythonCandidates(root: string | undefined): string[] { + if (!root || !existsSync(root)) { + return []; + } + + try { + return readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => join(root, entry.name, process.platform === "win32" ? "python.exe" : "bin/python3")) + .filter((candidate) => existsSync(candidate)) + .sort((left, right) => right.localeCompare(left, "en-US", { numeric: true, sensitivity: "base" })); + } catch { + return []; + } +} + +function childPythonCandidateDetails(root: string | undefined, source: string): PythonRuntimeCandidate[] { + return childPythonCandidates(root).map((path) => ({ path, source })); +} + +function systemPythonCandidates(): string[] { + return systemPythonCandidateDetails().map((candidate) => candidate.path); +} + +function systemPythonCandidateDetails(): PythonRuntimeCandidate[] { + if (process.platform !== "win32") { + return uniquePythonCandidates([ + ...pathPythonCandidates().map((path) => ({ path, source: "PATH" })), + { path: "/usr/bin/python3", source: "/usr/bin" }, + { path: "/usr/local/bin/python3", source: "/usr/local/bin" }, + { path: "/opt/homebrew/bin/python3", source: "Homebrew" }, + ]); + } + + const candidates: PythonRuntimeCandidate[] = []; + if (process.env.LOCALAPPDATA) { + candidates.push(...childPythonCandidateDetails(join(process.env.LOCALAPPDATA, "Programs", "Python"), "用户 Python")); + } + candidates.push(...childPythonCandidateDetails(process.env.ProgramFiles, "Program Files")); + candidates.push(...childPythonCandidateDetails(process.env["ProgramFiles(x86)"], "Program Files (x86)")); + if (process.env.USERPROFILE) { + candidates.push(...childPythonCandidateDetails(join(process.env.USERPROFILE, ".pyenv", "pyenv-win", "versions"), "pyenv-win")); + } + candidates.push(...pathPythonCandidates().map((path) => ({ path, source: "PATH" }))); + return uniquePythonCandidates(candidates); +} + +function systemGitCandidates(): string[] { + if (process.platform !== "win32") { + return [ + ...pathGitCandidates(), + "/usr/bin/git", + "/usr/local/bin/git", + "/opt/homebrew/bin/git", + ]; + } + + const candidates: string[] = []; + const addWindowsGitRoot = (root: string | undefined): void => { + if (!root) return; + candidates.push( + join(root, "Git", "cmd", "git.exe"), + join(root, "Git", "bin", "git.exe"), + join(root, "Git", "mingw64", "bin", "git.exe"), + ); + }; + + addWindowsGitRoot(process.env.ProgramFiles); + addWindowsGitRoot(process.env["ProgramFiles(x86)"]); + addWindowsGitRoot(process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "Programs") : undefined); + + if (process.env.USERPROFILE) { + candidates.push(join(process.env.USERPROFILE, "scoop", "apps", "git", "current", "cmd", "git.exe")); + } + if (process.env.ProgramData) { + candidates.push(join(process.env.ProgramData, "chocolatey", "bin", "git.exe")); + } + + candidates.push(...pathGitCandidates()); + return candidates; +} + +const AGREEMENT_FILES: Array<{ id: AgreementDocumentId; title: string; fileName: string; envVar: string }> = [ + { id: "eula", title: "最终用户许可协议", fileName: "EULA.md", envVar: "EULA_AGREE" }, + { id: "privacy", title: "隐私政策", fileName: "PRIVACY.md", envVar: "PRIVACY_AGREE" }, +]; + +const AGREEMENT_STORE_FILE = "agreement.json"; + +/** + * NapCat 鍚姩鍖呰 .cmd锛氬湪鍚姩 exe 鍓嶅厛鍒囨帶鍒跺彴鍒?UTF-8锛岄伩鍏嶄腑鏂囦贡鐮併€? + * 鍐呭鏄浐瀹氱殑銆佷笉渚濊禆浠讳綍杩愯鏃舵嫾鎺ョ殑鍙橀噺锛氫笉浼氶亣鍒?cmd 寮曞彿瑙f瀽闂銆? + */ +const NAPCAT_LAUNCHER_FILE = "napcat-launch.cmd"; +const NAPCAT_LAUNCHER_CONTENT = [ + "@echo off", + "chcp 65001 >nul", + 'cd /d "%~dp0"', + '"%~dp0NapCatWinBootMain.exe" %*', + "", +].join("\r\n"); + +const NAPCAT_ADAPTER_DIR = join("plugins", "napcat-adapter"); +const NAPCAT_ADAPTER_CONFIG_VERSION = "0.1.0"; +const NAPCAT_ADAPTER_PLUGIN_ID = "maibot-team.napcat-adapter"; +const SNOWLUMA_ADAPTER_DIR = join("plugins", "snowluma-adapter"); +const SNOWLUMA_ADAPTER_CONFIG_VERSION = "1.0.0"; +const SNOWLUMA_ADAPTER_PLUGIN_ID = "maibot-team.snowluma-adapter"; +const NAPCAT_ADAPTER_HOST = "127.0.0.1"; +const NAPCAT_ADAPTER_PORT = 7998; +const SNOWLUMA_ONEBOT_PORT = 7988; +const SNOWLUMA_WEBUI_PORT = 5099; +const LOCAL_CHAT_PLATFORM = "onekey-local-chat"; +const LOCAL_CHAT_BOT_ACCOUNT = "onekey-local-bot"; + +interface NapcatWebsocketServerConfig { + host: string; + port: number; + token: string; +} + +function buildDefaultNapcatAdapterConfig(token = "", port = NAPCAT_ADAPTER_PORT): NapcatAdapterConfig { + return { + plugin: { + enabled: true, + configVersion: NAPCAT_ADAPTER_CONFIG_VERSION, + }, + server: { + host: NAPCAT_ADAPTER_HOST, + port, + token, + heartbeatInterval: 30, + reconnectDelaySec: 5, + actionTimeoutSec: 15, + connectionId: "", + }, + chat: { + enableChatListFilter: true, + showDroppedChatListMessages: false, + groupListType: "whitelist", + groupList: [], + privateListType: "whitelist", + privateList: [], + banUserId: [], + banQqBot: false, + }, + filters: { + ignoreSelfMessage: true, + }, + }; +} + +function asString(value: unknown, fallback: string): string { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "bigint") return String(value); + return fallback; +} + +function asBool(value: unknown, fallback: boolean): boolean { + if (typeof value === "boolean") return value; + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (["true", "1", "yes", "on"].includes(normalized)) return true; + if (["false", "0", "no", "off"].includes(normalized)) return false; + } + return fallback; +} + +function asPositiveNumber(value: unknown, fallback: number): number { + if (typeof value === "number" && Number.isFinite(value) && value > 0) return value; + if (typeof value === "bigint" && value > 0n) return Number(value); + if (typeof value === "string") { + const parsed = Number.parseFloat(value); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + } + return fallback; +} + +function asPositiveInt(value: unknown, fallback: number): number { + const num = asPositiveNumber(value, fallback); + return Math.max(1, Math.floor(num)); +} + +function asListMode(value: unknown, fallback: NapcatChatListMode): NapcatChatListMode { + if (value === "whitelist" || value === "blacklist") return value; + return fallback; +} + +function asStringList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + const seen = new Set(); + const out: string[] = []; + for (const item of value) { + const text = typeof item === "string" ? item.trim() : String(item ?? "").trim(); + if (!text || seen.has(text)) continue; + seen.add(text); + out.push(text); + } + return out; +} + +function normalizeNapcatAdapterConfig( + raw: Record, + defaults: NapcatAdapterConfig, +): NapcatAdapterConfig { + const pluginRaw = (raw["plugin"] as Record | undefined) ?? {}; + const serverRaw = + ((raw["napcat_server"] ?? raw["luma_client"] ?? raw["connection"]) as Record | undefined) ?? {}; + const chatRaw = (raw["chat"] as Record | undefined) ?? {}; + const filtersRaw = (raw["filters"] as Record | undefined) ?? {}; + + return { + plugin: { + enabled: asBool(pluginRaw["enabled"], defaults.plugin.enabled), + configVersion: asString(pluginRaw["config_version"], defaults.plugin.configVersion), + }, + server: { + host: asString(serverRaw["host"], defaults.server.host).trim() || defaults.server.host, + port: asPositiveInt(serverRaw["port"], defaults.server.port), + token: asString(serverRaw["token"] ?? serverRaw["access_token"], defaults.server.token), + heartbeatInterval: asPositiveNumber( + serverRaw["heartbeat_interval"] ?? serverRaw["heartbeat_sec"], + defaults.server.heartbeatInterval, + ), + reconnectDelaySec: asPositiveNumber( + serverRaw["reconnect_delay_sec"], + defaults.server.reconnectDelaySec, + ), + actionTimeoutSec: asPositiveNumber( + serverRaw["action_timeout_sec"], + defaults.server.actionTimeoutSec, + ), + connectionId: asString(serverRaw["connection_id"], defaults.server.connectionId), + }, + chat: { + enableChatListFilter: asBool( + chatRaw["enable_chat_list_filter"], + defaults.chat.enableChatListFilter, + ), + showDroppedChatListMessages: asBool( + chatRaw["show_dropped_chat_list_messages"], + defaults.chat.showDroppedChatListMessages, + ), + groupListType: asListMode(chatRaw["group_list_type"], defaults.chat.groupListType), + groupList: asStringList(chatRaw["group_list"] ?? defaults.chat.groupList), + privateListType: asListMode(chatRaw["private_list_type"], defaults.chat.privateListType), + privateList: asStringList(chatRaw["private_list"] ?? defaults.chat.privateList), + banUserId: asStringList(chatRaw["ban_user_id"] ?? defaults.chat.banUserId), + banQqBot: asBool(chatRaw["ban_qq_bot"], defaults.chat.banQqBot), + }, + filters: { + ignoreSelfMessage: asBool(filtersRaw["ignore_self_message"], defaults.filters.ignoreSelfMessage), + }, + }; +} + +function napcatAdapterConfigToToml(config: NapcatAdapterConfig): string { + const document = { + plugin: { + enabled: config.plugin.enabled, + config_version: config.plugin.configVersion, + }, + napcat_server: { + host: config.server.host, + port: config.server.port, + token: config.server.token, + heartbeat_interval: config.server.heartbeatInterval, + reconnect_delay_sec: config.server.reconnectDelaySec, + action_timeout_sec: config.server.actionTimeoutSec, + connection_id: config.server.connectionId, + }, + chat: { + enable_chat_list_filter: config.chat.enableChatListFilter, + show_dropped_chat_list_messages: config.chat.showDroppedChatListMessages, + group_list_type: config.chat.groupListType, + group_list: config.chat.groupList, + private_list_type: config.chat.privateListType, + private_list: config.chat.privateList, + ban_user_id: config.chat.banUserId, + ban_qq_bot: config.chat.banQqBot, + }, + filters: { + ignore_self_message: config.filters.ignoreSelfMessage, + }, + } as const; + return stringifyToml(document); +} + +function snowlumaAdapterConfigToToml(config: NapcatAdapterConfig): string { + const document = { + plugin: { + enabled: config.plugin.enabled, + config_version: config.plugin.configVersion, + }, + luma_client: { + server: config.server.host, + port: config.server.port, + token: config.server.token, + connection_id: config.server.connectionId, + reconnect_delay_sec: config.server.reconnectDelaySec, + action_timeout_sec: config.server.actionTimeoutSec, + }, + chat: { + enable_chat_list_filter: config.chat.enableChatListFilter, + show_dropped_chat_list_messages: config.chat.showDroppedChatListMessages, + group_list_type: config.chat.groupListType, + group_list: config.chat.groupList, + private_list_type: config.chat.privateListType, + private_list: config.chat.privateList, + ban_user_id: config.chat.banUserId, + ban_qq_bot: config.chat.banQqBot, + }, + filters: { + ignore_self_message: config.filters.ignoreSelfMessage, + }, + } as const; + return stringifyToml(document); +} + +function applyChatOverrides( + base: NapcatAdapterChatConfig, + override?: Partial, +): NapcatAdapterChatConfig { + if (!override) return base; + return { + enableChatListFilter: override.enableChatListFilter ?? base.enableChatListFilter, + showDroppedChatListMessages: + override.showDroppedChatListMessages ?? base.showDroppedChatListMessages, + groupListType: override.groupListType ?? base.groupListType, + groupList: override.groupList !== undefined ? asStringList(override.groupList) : base.groupList, + privateListType: override.privateListType ?? base.privateListType, + privateList: override.privateList !== undefined ? asStringList(override.privateList) : base.privateList, + banUserId: override.banUserId !== undefined ? asStringList(override.banUserId) : base.banUserId, + banQqBot: override.banQqBot ?? base.banQqBot, + }; +} + +interface StoredAgreementFile { + version: 1; + hashes: Partial>; + confirmedAt?: number; +} + +interface StoredMessagePlatformFile { + version: 1; + backend?: QqBackend; + qqAccount?: string; + configuredAt?: number; + adapterConfigInitialized?: Partial>; +} + +function isDigits(value: string): boolean { + return /^\d+$/.test(value); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function ensureBotQqConfig(content: string, account: string): string { + return ensureBotPlatformConfig(content, { + platform: "qq", + qqAccount: account, + extraPlatformAccount: `${LOCAL_CHAT_PLATFORM}:${LOCAL_CHAT_BOT_ACCOUNT}`, + }); +} + +function ensureLocalChatBotConfig(content: string): string { + return ensureBotPlatformConfig(content, { + extraPlatformAccount: `${LOCAL_CHAT_PLATFORM}:${LOCAL_CHAT_BOT_ACCOUNT}`, + }); +} + +function ensureBotPlatformConfig( + content: string, + options: { platform?: string; qqAccount?: string; extraPlatformAccount: string }, +): string { + const botSectionMatch = content.match(/(^|\r?\n)(\s*\[bot\]\s*(?:#.*)?)(?:\r?\n|$)/u); + if (!botSectionMatch) { + const platformLine = options.platform ? `platform = "${options.platform}"\n` : ""; + const qqAccountLine = options.qqAccount ? `qq_account = ${options.qqAccount}\n` : ""; + return `${content.trimEnd()}\n\n[bot]\n${platformLine}${qqAccountLine}platforms = [\n "${options.extraPlatformAccount}",\n]\n`; + } + + const botSectionStart = (botSectionMatch.index ?? 0) + botSectionMatch[0].length; + const nextSectionOffset = content + .slice(botSectionStart) + .search(/\r?\n\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?(?:\r?\n|$)/u); + const botSectionEnd = + nextSectionOffset === -1 ? content.length : botSectionStart + nextSectionOffset; + const beforeBotSection = content.slice(0, botSectionStart); + const botSection = content.slice(botSectionStart, botSectionEnd); + const afterBotSection = content.slice(botSectionEnd); + let nextBotSection = botSection; + + if (options.platform) { + if (/^\s*platform\s*=/mu.test(nextBotSection)) { + nextBotSection = nextBotSection.replace( + /^\s*platform\s*=\s*["'][^"']*["'](\s*#.*)?$/mu, + `platform = "${options.platform}"$1`, + ); + } else { + nextBotSection = `platform = "${options.platform}"\n${nextBotSection}`; + } + } + + if (options.qqAccount) { + if (/^\s*qq_account\s*=/mu.test(nextBotSection)) { + nextBotSection = nextBotSection.replace( + /^\s*qq_account\s*=\s*["']?[^"'\r\n]+["']?(\s*#.*)?$/mu, + `qq_account = ${options.qqAccount}$1`, + ); + } else { + nextBotSection = `${nextBotSection.trimEnd()}\nqq_account = ${options.qqAccount}\n`; + } + } + + const platformsMatch = nextBotSection.match(/(^|\r?\n)(\s*platforms\s*=\s*)(\[[\s\S]*?\])(\s*(?:#.*)?)(?=\r?\n|$)/u); + const platformEntries = platformsMatch + ? Array.from(platformsMatch[3].matchAll(/["']([^"']+)["']/gu), (match) => match[1]) + : []; + const nextPlatformEntries = [ + ...platformEntries.filter((entry) => { + const [platformName] = entry.split(":", 1); + return platformName.trim().toLowerCase() !== LOCAL_CHAT_PLATFORM; + }), + options.extraPlatformAccount, + ]; + if (platformsMatch) { + const nextListBody = nextPlatformEntries.map((entry) => ` "${entry}",`).join("\n"); + nextBotSection = `${nextBotSection.slice(0, platformsMatch.index)}${platformsMatch[1] ?? ""}${platformsMatch[2]}[\n${nextListBody}\n]${platformsMatch[4]}${nextBotSection.slice((platformsMatch.index ?? 0) + platformsMatch[0].length)}`; + } else { + nextBotSection = `${nextBotSection.trimEnd()}\nplatforms = [\n "${options.extraPlatformAccount}",\n]\n`; + } + + return `${beforeBotSection}${nextBotSection}${afterBotSection}`; +} + +function checkFile(path: string, label: string, id: string): InitCheck { + if (existsSync(path)) { + return { id, label, status: "ok", detail: "已找到", path }; + } + + return { id, label, status: "error", detail: "缺失", path }; +} + +function checkDir(path: string, label: string, id: string): InitCheck { + if (existsSync(path)) { + return { id, label, status: "ok", detail: "已找到", path }; + } + + return { id, label, status: "error", detail: "缺失", path }; +} + +function runProcess(file: string, args: string[], cwd: string, timeoutMs = 8_000): Promise { + return new Promise((resolve, reject) => { + execFile( + file, + args, + { + cwd, + timeout: timeoutMs, + windowsHide: true, + env: { + ...process.env, + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + }, + }, + (error, stdout, stderr) => { + const output = `${stdout}${stderr}`.trim(); + if (error) { + reject(new Error(output || error.message)); + return; + } + + resolve(output); + }, + ); + }); +} + +function isCleanPipCheckOutput(output: string): boolean { + return /(?:^|\n)\s*No broken requirements found\.\s*$/iu.test(output.trim()); +} + +function toDetail(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function parsePyLauncherPaths(output: string): PythonRuntimeCandidate[] { + return output + .split(/\r?\n/u) + .map((line) => line.trim()) + .map((line) => { + const match = line.match(/(?:-\d+(?:\.\d+)?(?:-\d+)?\s+\*?\s*)?(.+?python(?:3)?\.exe)$/iu); + return match?.[1]?.trim(); + }) + .filter((path): path is string => Boolean(path)) + .map((path) => ({ path, source: "py launcher" })); +} + +function createWebsocketToken(): string { + return randomBytes(24).toString("base64url").slice(0, 32); +} + +function md5Utf8(content: string): string { + // 涓?Python `open(path, encoding="utf-8").read()` 琛屼负瀵归綈锛? + // Python 鏂囨湰妯″紡浼氭妸 \r\n / \r 缁熶竴杞垚 \n锛屽啀浜ょ粰 hashlib銆? + // Node 鐨?readFile(path, 'utf8') 淇濈暀鍘熷 CRLF锛屾墍浠ヨ繖閲屾墜鍔ㄥ綊涓€鍖栦互鍖归厤 MaiBot 鐨勫搱甯岀粨鏋溿€? + const normalized = content.replace(/\r\n?/g, "\n"); + return createHash("md5").update(normalized, "utf8").digest("hex"); +} + +function hasInnerVersion(content: string): boolean { + const innerMatch = content.match(/(^|\n)\s*\[inner\]\s*(?:\n|$)/u); + if (!innerMatch) { + return false; + } + + const sectionStart = (innerMatch.index ?? 0) + innerMatch[0].length; + const nextSection = content.slice(sectionStart).search(/\n\s*\[[^\]]+\]\s*(?:\n|$)/u); + const section = + nextSection === -1 + ? content.slice(sectionStart) + : content.slice(sectionStart, sectionStart + nextSection); + return /^\s*version\s*=\s*["'][^"']+["']\s*$/mu.test(section); +} + +function ensureInnerVersion(content: string, version: string): string { + if (hasInnerVersion(content)) { + return content; + } + + const innerMatch = content.match(/(^|\n)(\s*\[inner\]\s*)(?:\n|$)/u); + if (!innerMatch) { + return `[inner]\nversion = "${version}"\n\n${content.replace(/^\uFEFF/u, "")}`; + } + + const insertAt = (innerMatch.index ?? 0) + innerMatch[0].length; + return `${content.slice(0, insertAt)}version = "${version}"\n${content.slice(insertAt)}`; +} + +function maibotInitialConfigVersion(templateVersion: string): string { + const match = templateVersion.match(/^(.*?)(\d+)([^\d]*)$/u); + if (!match) { + return templateVersion; + } + + const current = Number(match[2]); + if (!Number.isSafeInteger(current) || current <= 0) { + return templateVersion; + } + + return `${match[1]}${current - 1}${match[3]}`; +} + +async function runWithoutAsar(operation: () => Promise): Promise { + const electronProcess = process as NodeJS.Process & { noAsar?: boolean }; + const previousNoAsar = electronProcess.noAsar; + electronProcess.noAsar = true; + + try { + return await operation(); + } finally { + electronProcess.noAsar = previousNoAsar; + } +} + +export class InitManager { + private dependencyCache?: { expiresAt: number; checks: InitCheck[] }; + + constructor(private readonly paths: RuntimePaths) {} + + getQqBackendSync(): QqBackend { + try { + const parsed = JSON.parse(readFileSync(this.qqBackendPath(), "utf8")) as { backend?: unknown }; + return parsed.backend === "snowluma" ? "snowluma" : "napcat"; + } catch { + return "napcat"; + } + } + + async readQqBackend(): Promise { + return this.getQqBackendSync(); + } + + hasMessagePlatformConfigured(): boolean { + return existsSync(this.messagePlatformPath()); + } + + async setQqBackend(backend: QqBackend, options: { syncAdapters?: boolean } = {}): Promise { + await mkdir(dirname(this.qqBackendPath()), { recursive: true }); + await writeFile( + this.qqBackendPath(), + `${JSON.stringify({ version: 1, backend, updatedAt: Date.now() }, null, 2)}\n`, + "utf8", + ); + await this.ensureServiceReady("napcat"); + if (options.syncAdapters !== false) { + const qqAccount = await this.readQqAccount(); + if (qqAccount && !(await this.isAdapterConfigInitialized(backend))) { + const syncedPaths = await this.syncSelectedQqAdapterConfigs(); + const selectedConfigPath = backend === "snowluma" + ? this.snowlumaAdapterConfigPath() + : this.napcatAdapterConfigPath(); + if (syncedPaths.some((path) => samePath(path, selectedConfigPath))) { + await this.markMessagePlatformConfigured(backend, qqAccount, backend); + } + } + } + } + + async getState(options: { refreshDependencies?: boolean } = {}): Promise { + const qqAccount = await this.readQqAccount(); + const qqBackend = await this.readQqBackend(); + const messagePlatformConfigured = this.hasMessagePlatformConfigured(); + const dependencyChecks = options.refreshDependencies === false + ? this.getCachedDependencyChecks() + : await this.checkDependencies(); + const napCatWebUiCheck = await this.checkNapCatWebUi(); + const qqModuleChecks = qqBackend === "snowluma" + ? [ + checkDir(this.paths.snowlumaRoot, "SnowLuma 模块", "snowluma-module"), + checkFile(join(this.paths.snowlumaRoot, "index.mjs"), "SnowLuma 启动文件", "snowluma-entry"), + ] + : [ + checkDir(this.paths.napcatRoot, "NapCat 模块", "napcat-module"), + checkFile( + join(this.paths.napcatRoot, "NapCatWinBootMain.exe"), + "NapCat 启动文件", + "napcat-entry", + ), + napCatWebUiCheck, + ]; + const checks: InitCheck[] = [ + this.checkRuntimeRoot(), + this.checkPythonRuntime(), + checkDir(this.paths.maibotRoot, "MaiBot 主模块", "maibot-module"), + checkFile(join(this.paths.maibotRoot, "bot.py"), "MaiBot 启动文件", "maibot-entry"), + ...qqModuleChecks, + ...dependencyChecks, + ]; + const isReady = checks.every((check) => check.status !== "error"); + return { isReady, qqAccount, qqBackend, messagePlatformConfigured, checks }; + } + + async refreshDependencyChecks(): Promise { + return this.checkDependencies(); + } + + async repair(): Promise { + const changedFiles = await this.ensureModulesReady(); + + const state = { + ...(await this.getState()), + repairedAt: Date.now(), + }; + return { state, changedFiles }; + } + + async getAgreementState(): Promise { + const stored = await this.readAgreementStore(); + const documents = await Promise.all( + AGREEMENT_FILES.map((agreement) => this.readAgreementDocument(agreement, stored)), + ); + return { + isConfirmed: documents.every((document) => document.exists && document.confirmed), + documents, + }; + } + + async confirmAgreements(): Promise { + const state = await this.getAgreementState(); + const missing = state.documents.find((document) => !document.exists); + if (missing) { + throw new Error(`${missing.title} 鏂囦欢缂哄け: ${missing.sourcePath}`); + } + + const hashes: Partial> = {}; + for (const document of state.documents) { + hashes[document.id] = document.hash; + } + + const storePath = this.agreementStorePath(); + const payload: StoredAgreementFile = { + version: 1, + hashes, + confirmedAt: Date.now(), + }; + await mkdir(dirname(storePath), { recursive: true }); + await writeFile(storePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); + + return { + state: await this.getAgreementState(), + changedFiles: [storePath], + }; + } + + async assertAgreementsConfirmed(): Promise { + const state = await this.getAgreementState(); + if (!state.isConfirmed) { + throw new Error("请先阅读并同意 MaiBot EULA 与隐私政策。"); + } + } + + /** + * 璁$畻褰撳墠 EULA / PRIVACY 鐨勬渶鏂?MD5锛屼綔涓虹幆澧冨彉閲忓湪姣忔鍚姩 MaiBot 鏃舵敞鍏ャ€? + * 楹﹂害鐨?bot.py 浼氳鍙?`EULA_AGREE` 涓?`PRIVACY_AGREE`锛岀瓑浜庡綋鍓嶆枃浠?hash 鍗宠涓哄凡鍚屾剰锛? + * 鍗忚鏈夋洿鏂版椂 hash 鑷姩鍙樺寲锛岄害楹︾浼氳Е鍙戦噸鏂扮‘璁ゆ祦绋嬨€? + */ + async getAgreementEnvVars(): Promise> { + const env: Record = {}; + for (const agreement of AGREEMENT_FILES) { + const sourcePath = this.agreementSourcePath(agreement.fileName); + if (!existsSync(sourcePath)) { + continue; + } + try { + const content = await readFile(sourcePath, "utf8"); + env[agreement.envVar] = md5Utf8(content); + } catch { + // 蹇界暐璇诲彇澶辫触锛岄害楹︿細鍥為€€鍒颁氦浜掑紡纭 + } + } + return env; + } + + getMaiBotDataDir(): string { + return join(this.paths.maibotRoot, "data"); + } + + getMaiBotConfigDir(): string { + return join(this.paths.maibotRoot, "config"); + } + + /** + * 鎶婄敤鎴锋彁渚涚殑 bot_config.toml / model_config.toml 瑕嗙洊鍒?MaiBot/config 涓嬶紝 + * 鑷姩鍑嗗濂藉彲鍐欑殑 MaiBot 妯″潡鐩綍涓?config 瀛愮洰褰曪紝骞跺鍘熸枃浠跺仛鏃堕棿鎴冲浠姐€? + */ + async importMaiBotConfig( + fileName: MaiBotConfigFileName, + sourcePath: string, + ): Promise { + if (fileName !== "bot_config.toml" && fileName !== "model_config.toml") { + throw new Error(`涓嶆敮鎸佺殑閰嶇疆鏂囦欢鍚? ${fileName}`); + } + if (!sourcePath) { + throw new Error("鏈€夋嫨閰嶇疆鏂囦欢"); + } + if (!existsSync(sourcePath)) { + throw new Error(`閰嶇疆鏂囦欢涓嶅瓨鍦? ${sourcePath}`); + } + const sourceStat = await stat(sourcePath); + if (!sourceStat.isFile()) { + throw new Error("选择的路径不是文件"); + } + + const configDir = this.getMaiBotConfigDir(); + await mkdir(configDir, { recursive: true }); + const destPath = join(configDir, fileName); + + let backupPath: string | undefined; + if (existsSync(destPath)) { + backupPath = `${destPath}.bak.${Date.now()}`; + await copyFile(destPath, backupPath); + } + + await copyFile(sourcePath, destPath); + + return { + fileName, + sourcePath, + destPath, + backupPath, + sizeBytes: sourceStat.size, + importedAt: Date.now(), + }; + } + + /** + * 鎶婄敤鎴锋彁渚涚殑 MaiBot.db 瑕嗙洊鍒?MaiBot/data/MaiBot.db锛? + * 鑷姩鍑嗗濂藉彲鍐欑殑 MaiBot 妯″潡鐩綍涓?data 瀛愮洰褰曘€? + */ + async importMaiBotDatabase(sourcePath: string): Promise { + if (!sourcePath) { + throw new Error("未选择数据库文件"); + } + if (!existsSync(sourcePath)) { + throw new Error(`鏁版嵁搴撴枃浠朵笉瀛樺湪: ${sourcePath}`); + } + const sourceStat = await stat(sourcePath); + if (!sourceStat.isFile()) { + throw new Error("选择的路径不是文件"); + } + + const dataDir = this.getMaiBotDataDir(); + await mkdir(dataDir, { recursive: true }); + const destPath = join(dataDir, "MaiBot.db"); + + let backupPath: string | undefined; + if (existsSync(destPath)) { + backupPath = `${destPath}.bak.${Date.now()}`; + await copyFile(destPath, backupPath); + } + + await copyFile(sourcePath, destPath); + + return { + sourcePath, + destPath, + backupPath, + sizeBytes: sourceStat.size, + importedAt: Date.now(), + }; + } + + /** + * 娓呯┖ MaiBot/data 鐩綍涓嬬殑鎵€鏈夊唴瀹癸紙涓嶄細鍒犻櫎 data 鐩綍鏈韩锛夈€? + * 浠呬綔鐢ㄤ簬鍙啓妯″潡鐩綍锛屽紑鍙戞€佹寚鍚?bundled 妯℃澘鏃朵細鎷掔粷鎵ц銆? + */ + async resetMaiBotData(): Promise { + if (samePath(this.paths.maibotRoot, join(this.paths.bundledModulesRoot, "MaiBot"))) { + throw new Error("当前指向内置模板目录,拒绝清空数据;请在打包后的环境执行。"); + } + + const dataDir = this.getMaiBotDataDir(); + if (!existsSync(dataDir)) { + return { dataDir, removedEntries: [], clearedAt: Date.now() }; + } + + const entries = await readdir(dataDir); + const removed: string[] = []; + for (const entry of entries) { + const target = join(dataDir, entry); + await rm(target, { recursive: true, force: true }); + removed.push(target); + } + + return { dataDir, removedEntries: removed, clearedAt: Date.now() }; + } + + async resetSnowLumaComponent(): Promise { + const bundledRoot = join(this.paths.bundledModulesRoot, "SnowLuma"); + const snowlumaRoot = this.paths.snowlumaRoot; + + if (!existsSync(bundledRoot)) { + throw new Error(`内置 SnowLuma 模板缺失: ${bundledRoot}`); + } + + if (samePath(bundledRoot, snowlumaRoot)) { + throw new Error("当前 SnowLuma 目录指向内置模板,拒绝重置。请在打包后的可写数据目录中执行。"); + } + + const removed = existsSync(snowlumaRoot); + await rm(snowlumaRoot, { recursive: true, force: true }); + await mkdir(dirname(snowlumaRoot), { recursive: true }); + await runWithoutAsar(() => + cp(bundledRoot, snowlumaRoot, { + recursive: true, + force: true, + errorOnExist: false, + }), + ); + + return { + snowlumaRoot, + bundledRoot, + removed, + copied: true, + resetAt: Date.now(), + }; + } + + private agreementStorePath(): string { + return join(this.paths.userDataRoot, AGREEMENT_STORE_FILE); + } + + private async readAgreementStore(): Promise { + const storePath = this.agreementStorePath(); + if (!existsSync(storePath)) { + return undefined; + } + try { + const raw = await readFile(storePath, "utf8"); + const parsed = JSON.parse(raw) as Partial; + if (!parsed || typeof parsed !== "object" || !parsed.hashes) { + return undefined; + } + return { + version: 1, + hashes: parsed.hashes, + confirmedAt: typeof parsed.confirmedAt === "number" ? parsed.confirmedAt : undefined, + }; + } catch { + return undefined; + } + } + + async setQqAccount( + qqAccount: string, + websocketToken?: string, + chatOverrides?: Partial, + qqBackend: QqBackend = "napcat", + ): Promise { + if (!isDigits(qqAccount)) { + throw new Error("QQ 号必须是纯数字"); + } + + const botConfigPath = this.botConfigPath(); + let content = await this.readOrCreateBotConfigContent(); + content = ensureBotQqConfig(content, qqAccount); + + await mkdir(dirname(botConfigPath), { recursive: true }); + await writeFile(botConfigPath, content, "utf8"); + await this.setQqBackend(qqBackend, { syncAdapters: false }); + const existingWebsocketServer = qqBackend === "snowluma" + ? await this.readSnowLumaWebsocketServer(qqAccount) + : await this.readNapcatWebsocketServer(qqAccount); + const resolvedWebsocketServer: NapcatWebsocketServerConfig = { + ...(existingWebsocketServer ?? { + host: NAPCAT_ADAPTER_HOST, + token: websocketToken || createWebsocketToken(), + }), + host: NAPCAT_ADAPTER_HOST, + port: qqBackend === "snowluma" ? SNOWLUMA_ONEBOT_PORT : NAPCAT_ADAPTER_PORT, + token: existingWebsocketServer?.token || websocketToken || createWebsocketToken(), + }; + const shouldInitializeAdapterConfig = !(await this.isAdapterConfigInitialized(qqBackend)); + let initializedAdapterConfig = false; + + if (qqBackend === "snowluma") { + await this.createSnowLumaConfigs(qqAccount, resolvedWebsocketServer.token, resolvedWebsocketServer.port); + } else { + await this.createNapCatConfigs(qqAccount, resolvedWebsocketServer.token, resolvedWebsocketServer.port); + await this.ensureNapCatWebUiConfig(); + } + if (shouldInitializeAdapterConfig) { + initializedAdapterConfig = await this.writeQqAdapterConfigsForBackend( + qqBackend, + resolvedWebsocketServer, + qqAccount, + chatOverrides, + ); + } + await this.markMessagePlatformConfigured( + qqBackend, + qqAccount, + initializedAdapterConfig ? qqBackend : undefined, + ); + return this.getState(); + } + + napcatAdapterConfigPath(): string { + return join( + this.findMaiBotPluginDirByManifestId(NAPCAT_ADAPTER_PLUGIN_ID) ?? join(this.paths.maibotRoot, NAPCAT_ADAPTER_DIR), + "config.toml", + ); + } + + snowlumaAdapterConfigPath(): string { + return join( + this.findMaiBotPluginDirByManifestId(SNOWLUMA_ADAPTER_PLUGIN_ID) ?? join(this.paths.maibotRoot, SNOWLUMA_ADAPTER_DIR), + "config.toml", + ); + } + + /** + * 璇诲彇鏈€鏂颁竴浠?onebot11_.json 涓凡鍐欏叆鐨?WebSocket Token锛? + * 鐢ㄤ簬鍦?napcat-adapter 閰嶇疆涓鐢ㄥ悓涓€涓?token锛岄伩鍏嶉害楹︾杩炰笉涓娿€? + */ + async readNapcatWebsocketServer(qqAccount?: string): Promise { + try { + const configDirs = await this.findNapCatRuntimeConfigDirs(); + const onebotPattern = qqAccount + ? new RegExp(`^onebot11_${escapeRegExp(qqAccount)}\\.json$`, "i") + : /^onebot11_\d+\.json$/i; + for (const configDir of configDirs) { + if (!existsSync(configDir)) continue; + const entries = await readdir(configDir); + const onebotFile = entries.find((name) => onebotPattern.test(name)); + if (!onebotFile) continue; + const raw = await readFile(join(configDir, onebotFile), "utf8"); + const parsed = JSON.parse(raw) as { + network?: { websocketServers?: Array<{ host?: string; token?: string; port?: number }> }; + }; + const server = + parsed?.network?.websocketServers?.find((entry) => entry?.port === NAPCAT_ADAPTER_PORT && entry?.token) ?? + parsed?.network?.websocketServers?.find((entry) => entry?.token); + if (server?.token) { + return { + host: server.host ? String(server.host) : NAPCAT_ADAPTER_HOST, + port: Number.isFinite(server.port) && server.port ? Math.floor(server.port) : NAPCAT_ADAPTER_PORT, + token: String(server.token), + }; + } + } + } catch { + // ignore 鈥?fall through to empty token + } + return undefined; + } + + async readNapcatWebsocketToken(qqAccount?: string): Promise { + return (await this.readNapcatWebsocketServer(qqAccount))?.token ?? ""; + } + + async readSnowLumaWebsocketServer(qqAccount?: string): Promise { + if (!qqAccount) { + return undefined; + } + + try { + const raw = await readFile(join(this.paths.snowlumaRoot, "config", `onebot_${qqAccount}.json`), "utf8"); + const parsed = JSON.parse(raw) as { + networks?: { + wsServers?: Array<{ host?: string; port?: number; accessToken?: string }>; + }; + }; + const server = + parsed.networks?.wsServers?.find((entry) => entry?.port === SNOWLUMA_ONEBOT_PORT && entry?.accessToken) ?? + parsed.networks?.wsServers?.find((entry) => entry?.accessToken) ?? + parsed.networks?.wsServers?.[0]; + if (!server?.port) { + return undefined; + } + return { + host: server.host ? String(server.host) : NAPCAT_ADAPTER_HOST, + port: Number.isFinite(server.port) ? Math.floor(server.port) : SNOWLUMA_ONEBOT_PORT, + token: server.accessToken ? String(server.accessToken) : "", + }; + } catch { + return undefined; + } + } + + /** + * 鍒涘缓/鏇存柊 napcat-adapter 鐨?config.toml锛? * token 鐩存帴鏉ヨ嚜褰撳墠 setQqAccount 娴佺▼鐢熸垚鐨?websocket token锛? + * chat 璁剧疆鍒欏彇鐢ㄦ埛鍦ㄥ紩瀵肩晫闈㈠~鍐欑殑瑕嗙洊鍊硷紙缂虹渷鍗抽粯璁わ級銆? + */ + private async writeQqAdapterConfigsForBackend( + qqBackend: QqBackend, + selectedWebsocketServer: NapcatWebsocketServerConfig, + qqAccount?: string, + chatOverrides?: Partial, + ): Promise { + const napcatServer = qqBackend === "napcat" + ? selectedWebsocketServer + : await this.resolveNapcatAdapterServer(qqAccount); + const snowlumaServer = qqBackend === "snowluma" + ? selectedWebsocketServer + : await this.resolveSnowLumaAdapterServer(qqAccount); + + const shouldInitializeInactive = !(await this.isAdapterConfigInitialized( + qqBackend === "snowluma" ? "napcat" : "snowluma", + )); + + if (qqBackend === "snowluma") { + if (shouldInitializeInactive) { + await this.writeNapcatAdapterConfigForServer(napcatServer, chatOverrides, false); + } + return this.writeSnowLumaAdapterConfigForServer(snowlumaServer, chatOverrides, true); + } + + const wroteSelected = await this.writeNapcatAdapterConfigForServer(napcatServer, chatOverrides, true); + if (shouldInitializeInactive) { + await this.writeSnowLumaAdapterConfigForServer(snowlumaServer, chatOverrides, false); + } + return wroteSelected; + } + + private async resolveNapcatAdapterServer(qqAccount?: string): Promise { + const existing = await this.readNapcatWebsocketServer(qqAccount); + return { + host: NAPCAT_ADAPTER_HOST, + port: NAPCAT_ADAPTER_PORT, + token: existing?.token || createWebsocketToken(), + }; + } + + private async resolveSnowLumaAdapterServer(qqAccount?: string): Promise { + const existing = await this.readSnowLumaWebsocketServer(qqAccount); + return { + host: NAPCAT_ADAPTER_HOST, + port: SNOWLUMA_ONEBOT_PORT, + token: existing?.token || createWebsocketToken(), + }; + } + + private async syncSelectedQqAdapterConfigs(): Promise { + const qqAccount = await this.readQqAccount(); + if (!qqAccount) { + return []; + } + + const qqBackend = await this.readQqBackend(); + let websocketServer = qqBackend === "snowluma" + ? await this.readSnowLumaWebsocketServer(qqAccount) + : await this.readNapcatWebsocketServer(qqAccount); + + websocketServer = { + host: NAPCAT_ADAPTER_HOST, + port: qqBackend === "snowluma" ? SNOWLUMA_ONEBOT_PORT : NAPCAT_ADAPTER_PORT, + token: websocketServer?.token || createWebsocketToken(), + }; + if (qqBackend === "snowluma") { + await this.createSnowLumaConfigs(qqAccount, websocketServer.token, websocketServer.port); + } else { + await this.createNapCatConfigs(qqAccount, websocketServer.token, websocketServer.port); + await this.ensureNapCatWebUiConfig(); + } + + await this.writeQqAdapterConfigsForBackend(qqBackend, websocketServer, qqAccount); + return [ + this.napcatAdapterConfigPath(), + this.snowlumaAdapterConfigPath(), + ].filter((path) => existsSync(path)); + } + + private async writeNapcatAdapterConfigForServer( + websocketServer: NapcatWebsocketServerConfig, + chatOverrides?: Partial, + enabled = true, + ): Promise { + const defaults = buildDefaultNapcatAdapterConfig(websocketServer.token, websocketServer.port); + let existing: NapcatAdapterConfig = defaults; + const configPath = this.napcatAdapterConfigPath(); + const adapterRoot = dirname(configPath); + + if (!existsSync(adapterRoot)) { + return false; + } + + if (existsSync(configPath)) { + try { + const text = await readFile(configPath, "utf8"); + const parsed = parseToml(text); + if (parsed && typeof parsed === "object") { + existing = normalizeNapcatAdapterConfig(parsed as Record, defaults); + } + } catch { + // 瑙f瀽澶辫触鍒欑洿鎺ヤ互榛樿鍊艰鐩? + } + } + + const merged: NapcatAdapterConfig = { + ...existing, + plugin: { + enabled, + configVersion: NAPCAT_ADAPTER_CONFIG_VERSION, + }, + server: { + ...existing.server, + host: websocketServer.host, + port: websocketServer.port, + token: websocketServer.token, + }, + chat: applyChatOverrides(existing.chat, chatOverrides), + }; + + await writeFile(configPath, napcatAdapterConfigToToml(merged), "utf8"); + return true; + } + + private async writeSnowLumaAdapterConfigForServer( + websocketServer: NapcatWebsocketServerConfig, + chatOverrides?: Partial, + enabled = true, + ): Promise { + const defaults = buildDefaultNapcatAdapterConfig(websocketServer.token, websocketServer.port); + defaults.plugin.configVersion = SNOWLUMA_ADAPTER_CONFIG_VERSION; + defaults.server.actionTimeoutSec = 10; + + let existing: NapcatAdapterConfig = defaults; + const configPath = this.snowlumaAdapterConfigPath(); + const adapterRoot = dirname(configPath); + + if (!existsSync(adapterRoot)) { + return false; + } + + if (existsSync(configPath)) { + try { + const text = await readFile(configPath, "utf8"); + const parsed = parseToml(text); + if (parsed && typeof parsed === "object") { + existing = normalizeNapcatAdapterConfig(parsed as Record, defaults); + } + } catch { + // 解析失败则直接以默认值覆盖 + } + } + + const merged: NapcatAdapterConfig = { + ...existing, + plugin: { + enabled, + configVersion: SNOWLUMA_ADAPTER_CONFIG_VERSION, + }, + server: { + ...existing.server, + host: websocketServer.host, + port: websocketServer.port, + token: websocketServer.token, + }, + chat: applyChatOverrides(existing.chat, chatOverrides), + }; + + await writeFile(configPath, snowlumaAdapterConfigToToml(merged), "utf8"); + return true; + } + + async ensureModulesReady(): Promise { + return [ + ...(await this.ensureServiceReady("maibot")), + ...(await this.ensureServiceReady("napcat")), + ...(await this.ensureBundledPythonOverrides()), + ]; + } + + async ensureServiceReady(serviceId: ServiceId): Promise { + await mkdir(this.paths.logsRoot, { recursive: true }); + + if (!existsSync(this.paths.bundledModulesRoot)) { + throw new Error(`鍐呯疆 modules 妯℃澘缂哄け: ${this.paths.bundledModulesRoot}`); + } + + if (serviceId === "maibot") { + const changedFiles = await this.ensureBundledModuleSubtree("MaiBot", ["bot.py"], { + excludeRelativePaths: [NAPCAT_ADAPTER_DIR, SNOWLUMA_ADAPTER_DIR], + }); + changedFiles.push(...(await this.ensureBundledMaiBotPluginSubtree(NAPCAT_ADAPTER_DIR, ["plugin.py"], NAPCAT_ADAPTER_PLUGIN_ID))); + changedFiles.push(...(await this.ensureBundledMaiBotPluginSubtree(SNOWLUMA_ADAPTER_DIR, ["plugin.py"], SNOWLUMA_ADAPTER_PLUGIN_ID))); + const repairedConfig = await this.repairBotConfigVersionInfo(); + return [...changedFiles, ...(repairedConfig ? [repairedConfig] : [])]; + } + + const qqBackend = await this.readQqBackend(); + if (qqBackend === "snowluma") { + return this.ensureBundledModuleSubtree("SnowLuma", [ + "node.exe", + "index.mjs", + "launcher.bat", + ], { + excludeRelativePaths: ["config", "data", "logs"], + }); + } + + const changedFiles = [ + ...(await this.ensureBundledModuleSubtree("napcat", [ + "node.exe", + "index.js", + join("napcat", "package.json"), + ], { + excludeRelativePaths: [ + "config", + "data", + "logs", + join("napcat", "config"), + join("napcat", "data"), + join("napcat", "logs"), + ], + })), + ...(await this.ensureBundledModuleSubtree("napcatframework", ["versions"], { + optional: true, + excludeRelativePaths: ["config", "data", "logs"], + })), + ]; + const launcher = await this.ensureNapCatLauncher(); + if (launcher) { + changedFiles.push(launcher); + } + return changedFiles; + } + + async ensureBundledPythonOverrides(): Promise { + const bundledRoot = join(dirname(this.paths.runtimeRoot), "python-overrides"); + const targetRoot = this.paths.pythonOverridesRoot; + if (!existsSync(bundledRoot) || samePath(bundledRoot, targetRoot)) { + return []; + } + + let bundledEntries: string[]; + try { + bundledEntries = (await readdir(bundledRoot)).filter((entry) => !PYTHON_OVERRIDES_IGNORED_ENTRIES.has(entry)); + } catch { + return []; + } + if (bundledEntries.length === 0) { + return []; + } + + let targetEntries: string[] = []; + try { + targetEntries = (await readdir(targetRoot)).filter((entry) => !PYTHON_OVERRIDES_IGNORED_ENTRIES.has(entry)); + } catch { + targetEntries = []; + } + if (targetEntries.length > 0) { + return []; + } + + await mkdir(dirname(targetRoot), { recursive: true }); + await runWithoutAsar(() => + cp(bundledRoot, targetRoot, { + recursive: true, + force: false, + errorOnExist: false, + }), + ); + return [targetRoot]; + } + + /** + * 鍦?napcat 鐩綍涓嬬敓鎴愪竴涓浐瀹氱殑寮曞 .cmd锛屽惎鍔ㄦ椂鍏?chcp 65001 鍐嶈皟 exe锛? + * 閬垮厤鍦ㄦ簮鐮侀噷鎷兼帴 `cmd /C` 瀛楃涓插甫鏉ョ殑寮曞彿闂锛屽悓鏃朵繚鐣欐帶鍒跺彴 UTF-8 + * 浠ュ厤涓枃杈撳嚭涔辩爜銆? + */ + private async ensureNapCatLauncher(): Promise { + const napcatRoot = this.paths.napcatRoot; + if (!existsSync(napcatRoot)) { + return undefined; + } + + const launcherPath = join(napcatRoot, NAPCAT_LAUNCHER_FILE); + const desired = NAPCAT_LAUNCHER_CONTENT; + + if (existsSync(launcherPath)) { + try { + const current = await readFile(launcherPath, "utf8"); + if (current === desired) { + return undefined; + } + } catch { + // 璇讳笉鍒板氨閲嶅啓 + } + } + + await writeFile(launcherPath, desired, "utf8"); + return launcherPath; + } + + async readQqAccount(): Promise { + const botConfigPath = this.botConfigPath(); + if (!existsSync(botConfigPath)) { + return undefined; + } + + const content = await readFile(botConfigPath, "utf8"); + const match = content.match(QQ_PATTERN); + return match?.[1]; + } + + getPythonPath(): string { + const bundledPython = this.getBundledPythonPath(); + if (bundledPython) { + return bundledPython; + } + + return this.findSystemPythonPath() ?? this.getBundledPythonCandidates()[0]; + } + + async listSystemPythonRuntimeCandidates(): Promise { + const candidates = systemPythonCandidateDetails(); + if (process.platform !== "win32") { + return candidates; + } + + try { + const output = await runProcess("py", ["-0p"], this.paths.installRoot, 3_000); + return uniquePythonCandidates([...parsePyLauncherPaths(output), ...candidates]); + } catch { + return candidates; + } + } + + getGitPath(): string { + const bundledGit = this.getBundledGitPath(); + if (bundledGit) { + return bundledGit; + } + + return this.findSystemGitPath() ?? this.getBundledGitCandidates()[0]; + } + + private getPythonRoot(): string { + return join(this.paths.runtimeRoot, PYTHON_RUNTIME_DIR); + } + + private getBundledPythonCandidates(): string[] { + const root = this.getPythonRoot(); + return [ + join(root, "python.exe"), + join(root, "bin", "python.exe"), + join(root, "python"), + join(root, "bin", "python3"), + join(root, "bin", "python"), + ]; + } + + private getBundledPythonPath(): string | undefined { + return uniqueExistingPaths(this.getBundledPythonCandidates())[0]; + } + + private findSystemPythonPath(): string | undefined { + return uniqueExistingPaths(systemPythonCandidates())[0]; + } + + private getGitRoot(): string { + return join(this.paths.runtimeRoot, GIT_RUNTIME_DIR); + } + + private getBundledGitCandidates(): string[] { + const root = this.getGitRoot(); + return [ + join(root, "bin", "git.exe"), + join(root, "cmd", "git.exe"), + join(root, "git.exe"), + join(root, "bin", "git"), + ]; + } + + private getBundledGitPath(): string | undefined { + return uniqueExistingPaths(this.getBundledGitCandidates())[0]; + } + + private findSystemGitPath(): string | undefined { + return uniqueExistingPaths(systemGitCandidates())[0]; + } + + private checkRuntimeRoot(): InitCheck { + if (existsSync(this.paths.runtimeRoot)) { + return { + id: "runtime", + label: "内置 runtime", + status: "ok", + detail: "已找到", + path: this.paths.runtimeRoot, + }; + } + + return { + id: "runtime", + label: "内置 runtime", + status: "warning", + detail: "未找到内置 runtime,将使用系统 Python 与 Git", + path: this.paths.runtimeRoot, + }; + } + + private checkPythonRuntime(): InitCheck { + const bundledPython = this.getBundledPythonPath(); + if (bundledPython) { + return { + id: "python-runtime", + label: "Python 运行时", + status: "ok", + detail: "使用内置 Python", + path: bundledPython, + }; + } + + const systemPython = this.findSystemPythonPath(); + if (systemPython) { + return { + id: "python-runtime", + label: "Python 运行时", + status: "ok", + detail: `使用系统 Python,后台检查版本是否 >= ${PYTHON_MINIMUM_VERSION}`, + path: systemPython, + }; + } + + return { + id: "python-runtime", + label: "Python 运行时", + status: "error", + detail: `未找到内置 Python 或系统 Python ${PYTHON_MINIMUM_VERSION}+`, + path: this.getBundledPythonCandidates()[0], + actionLabel: "下载 Python", + actionUrl: PYTHON_DOWNLOAD_URL, + }; + } + + private async ensureBundledModuleSubtree( + moduleName: string, + requiredRelativePaths: string[], + optionalOrOptions: boolean | { optional?: boolean; excludeRelativePaths?: string[] } = false, + ): Promise { + const options = typeof optionalOrOptions === "boolean" ? { optional: optionalOrOptions } : optionalOrOptions; + const source = join(this.paths.bundledModulesRoot, moduleName); + const target = this.moduleTargetRoot(moduleName); + + if (!existsSync(source)) { + if (options.optional) { + return []; + } + + throw new Error(`鍐呯疆 ${moduleName} 妯℃澘缂哄け: ${source}`); + } + + if (samePath(source, target)) { + return []; + } + + const isReady = requiredRelativePaths.every((relativePath) => existsSync(join(target, relativePath))); + if (isReady) { + return []; + } + + await mkdir(dirname(target), { recursive: true }); + const excludedSources = (options.excludeRelativePaths ?? []).map((relativePath) => join(source, relativePath)); + await runWithoutAsar(() => + cp(source, target, { + recursive: true, + force: false, + errorOnExist: false, + filter: (sourcePath) => !excludedSources.some((excludedSource) => sameOrInsidePath(excludedSource, sourcePath)), + }), + ); + + return [target]; + } + + private async ensureBundledMaiBotPluginSubtree( + pluginRelativePath: string, + requiredRelativePaths: string[], + expectedPluginId?: string, + ): Promise { + const source = join(this.paths.bundledModulesRoot, "MaiBot", pluginRelativePath); + const target = join(this.paths.maibotRoot, pluginRelativePath); + + if (!existsSync(source) || samePath(source, target)) { + return []; + } + + const sourcePluginId = expectedPluginId ?? this.readPluginManifestId(source); + if (sourcePluginId && this.findMaiBotPluginDirByManifestId(sourcePluginId)) { + return []; + } + + const isReady = requiredRelativePaths.every((relativePath) => existsSync(join(target, relativePath))); + if (isReady) { + return []; + } + + await mkdir(dirname(target), { recursive: true }); + await runWithoutAsar(() => + cp(source, target, { + recursive: true, + force: false, + errorOnExist: false, + }), + ); + return [target]; + } + + private findMaiBotPluginDirByManifestId(pluginId: string): string | undefined { + const pluginsRoot = join(this.paths.maibotRoot, "plugins"); + if (!existsSync(pluginsRoot)) { + return undefined; + } + + try { + for (const entry of readdirSync(pluginsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const pluginDir = join(pluginsRoot, entry.name); + if (this.readPluginManifestId(pluginDir) === pluginId) { + return pluginDir; + } + } + } catch { + return undefined; + } + + return undefined; + } + + private readPluginManifestId(pluginDir: string): string | undefined { + const manifestPath = join(pluginDir, "_manifest.json"); + if (!existsSync(manifestPath)) { + return undefined; + } + + try { + const parsed = JSON.parse(readFileSync(manifestPath, "utf8")) as { id?: unknown }; + return typeof parsed.id === "string" && parsed.id.trim() ? parsed.id.trim() : undefined; + } catch { + return undefined; + } + } + + private moduleTargetRoot(moduleName: string): string { + if (moduleName === "MaiBot") { + return this.paths.maibotRoot; + } + if (moduleName === "napcat") { + return this.paths.napcatRoot; + } + if (moduleName === "napcatframework") { + return this.napcatFrameworkRoot(); + } + if (moduleName === "SnowLuma") { + return this.paths.snowlumaRoot; + } + return join(this.paths.modulesRoot, moduleName); + } + + private napcatFrameworkRoot(): string { + return join(dirname(this.paths.napcatRoot), "napcatframework"); + } + + async ensureNapCatWebUiConfig(): Promise { + const existing = await this.readNapCatWebUiToken(); + if (existing.token) { + return existing.token; + } + + if (existing.exists) { + throw new Error(existing.error ?? "NapCat WebUI 閰嶇疆瀛樺湪浣嗙己灏?token锛岃鎵嬪姩妫€鏌?webui.json"); + } + + const configDirs = await this.findNapCatWebUiConfigDirs(); + if (configDirs.length === 0) { + return undefined; + } + + const token = randomBytes(6).toString("hex"); + const defaultJson = { + host: "0.0.0.0", + port: 6099, + token, + loginRate: 10, + autoLoginAccount: "", + theme: { dark: {}, light: {} }, + disableWebUI: false, + disableNonLANAccess: false, + }; + + for (const configDir of configDirs) { + const target = join(configDir, "webui.json"); + if (existsSync(target)) { + continue; + } + + await mkdir(configDir, { recursive: true }); + await writeFile(target, JSON.stringify(defaultJson, null, 2), "utf8"); + } + + return token; + } + + async readNapCatWebUiToken(): Promise<{ token?: string; exists: boolean; error?: string }> { + const candidates = await this.findNapCatWebUiFiles(); + let sawExisting = false; + let firstError: string | undefined; + + for (const candidate of candidates) { + if (!existsSync(candidate)) { + continue; + } + + sawExisting = true; + try { + const raw = JSON.parse(await readFile(candidate, "utf8")) as { token?: unknown }; + if (typeof raw.token === "string" && raw.token.length > 0) { + return { token: raw.token, exists: true }; + } + firstError ??= `缂哄皯 token: ${candidate}`; + } catch (error) { + firstError ??= `JSON 鏍煎紡閿欒: ${candidate}: ${toDetail(error)}`; + } + } + + return { exists: sawExisting, error: firstError }; + } + + /** + * 璇诲彇 MaiBot Core WebUI 鐨?access_token锛岀敤浜庡湪 WebUI 鍏ュ彛鎷兼帴 + * `?token=` 瀹炵幇鑷姩鐧诲綍銆? + * 鏂囦欢涓嶅瓨鍦ㄦ垨缂哄瓧娈垫椂杩斿洖绌?token锛岃皟鐢ㄦ柟搴斿洖閫€涓轰笉甯﹀弬鏁扮殑鍦板潃銆? + */ + async readMaiBotWebUiToken(): Promise<{ token?: string; exists: boolean; error?: string }> { + const candidates = [ + join(this.paths.maibotRoot, "data", "webui.json"), + join(this.paths.bundledModulesRoot, "MaiBot", "data", "webui.json"), + ]; + + let sawExisting = false; + let firstError: string | undefined; + + for (const candidate of candidates) { + if (!existsSync(candidate)) { + continue; + } + + sawExisting = true; + try { + const raw = JSON.parse(await readFile(candidate, "utf8")) as { access_token?: unknown }; + if (typeof raw.access_token === "string" && raw.access_token.length > 0) { + return { token: raw.access_token, exists: true }; + } + firstError ??= `缂哄皯 access_token: ${candidate}`; + } catch (error) { + firstError ??= `JSON 鏍煎紡閿欒: ${candidate}: ${toDetail(error)}`; + } + } + + return { exists: sawExisting, error: firstError }; + } + + private botConfigPath(): string { + return join(this.paths.maibotRoot, "config", "bot_config.toml"); + } + + private qqBackendPath(): string { + return join(this.paths.userDataRoot, QQ_BACKEND_FILE); + } + + private messagePlatformPath(): string { + return join(this.paths.userDataRoot, MESSAGE_PLATFORM_FILE); + } + + private readMessagePlatformStore(): StoredMessagePlatformFile | undefined { + try { + const parsed = JSON.parse(readFileSync(this.messagePlatformPath(), "utf8")) as Partial; + const backend = parsed.backend === "snowluma" ? "snowluma" : parsed.backend === "napcat" ? "napcat" : undefined; + const initialized = parsed.adapterConfigInitialized && typeof parsed.adapterConfigInitialized === "object" + ? parsed.adapterConfigInitialized + : {}; + return { + version: 1, + backend, + qqAccount: typeof parsed.qqAccount === "string" ? parsed.qqAccount : undefined, + configuredAt: typeof parsed.configuredAt === "number" ? parsed.configuredAt : undefined, + adapterConfigInitialized: { + napcat: typeof initialized.napcat === "number" ? initialized.napcat : undefined, + snowluma: typeof initialized.snowluma === "number" ? initialized.snowluma : undefined, + }, + }; + } catch { + return undefined; + } + } + + private async isAdapterConfigInitialized(backend: QqBackend): Promise { + const configPath = backend === "snowluma" + ? this.snowlumaAdapterConfigPath() + : this.napcatAdapterConfigPath(); + return ( + typeof this.readMessagePlatformStore()?.adapterConfigInitialized?.[backend] === "number" + || existsSync(configPath) + ); + } + + private async markMessagePlatformConfigured( + backend: QqBackend, + qqAccount: string, + initializedBackend?: QqBackend, + ): Promise { + const existing = this.readMessagePlatformStore(); + const adapterConfigInitialized = { + ...(existing?.adapterConfigInitialized ?? {}), + }; + if (initializedBackend) { + adapterConfigInitialized[initializedBackend] = Date.now(); + } + + await mkdir(dirname(this.messagePlatformPath()), { recursive: true }); + await writeFile( + this.messagePlatformPath(), + `${JSON.stringify({ + version: 1, + backend, + qqAccount, + configuredAt: existing?.configuredAt ?? Date.now(), + updatedAt: Date.now(), + adapterConfigInitialized, + }, null, 2)}\n`, + "utf8", + ); + } + + private agreementSourcePath(fileName: string): string { + const writablePath = join(this.paths.maibotRoot, fileName); + if (existsSync(writablePath)) { + return writablePath; + } + + return join(this.paths.bundledModulesRoot, "MaiBot", fileName); + } + + private async readAgreementDocument( + { + id, + title, + fileName, + }: { + id: AgreementDocumentId; + title: string; + fileName: string; + envVar: string; + }, + stored: StoredAgreementFile | undefined, + ): Promise { + const sourcePath = this.agreementSourcePath(fileName); + const confirmPath = this.agreementStorePath(); + if (!existsSync(sourcePath)) { + return { + id, + title, + fileName, + sourcePath, + confirmPath, + content: "", + hash: "", + exists: false, + confirmed: false, + error: `${fileName} 鏂囦欢缂哄け`, + }; + } + + try { + const content = await readFile(sourcePath, "utf8"); + const hash = md5Utf8(content); + const confirmed = stored?.hashes?.[id] === hash; + return { + id, + title, + fileName, + sourcePath, + confirmPath, + content, + hash, + exists: true, + confirmed, + }; + } catch (error) { + return { + id, + title, + fileName, + sourcePath, + confirmPath, + content: "", + hash: "", + exists: false, + confirmed: false, + error: toDetail(error), + }; + } + } + + private async readOrCreateBotConfigContent(): Promise { + const botConfigPath = this.botConfigPath(); + const configVersion = maibotInitialConfigVersion(await this.readMaiBotConfigVersion()); + if (!existsSync(botConfigPath)) { + return ensureLocalChatBotConfig(`[inner]\nversion = "${configVersion}"\n\n[bot]\nplatform = "qq"\n`); + } + + const content = await readFile(botConfigPath, "utf8"); + return ensureLocalChatBotConfig(ensureInnerVersion(content, configVersion)); + } + + private async repairBotConfigVersionInfo(): Promise { + const botConfigPath = this.botConfigPath(); + if (!existsSync(botConfigPath)) { + return undefined; + } + + const content = await readFile(botConfigPath, "utf8"); + const repaired = ensureLocalChatBotConfig( + ensureInnerVersion(content, maibotInitialConfigVersion(await this.readMaiBotConfigVersion())), + ); + if (repaired === content) { + return undefined; + } + + await writeFile(botConfigPath, repaired, "utf8"); + return botConfigPath; + } + + private async readMaiBotConfigVersion(): Promise { + const candidates = [ + join(this.paths.maibotRoot, "src", "config", "config.py"), + join(this.paths.bundledModulesRoot, "MaiBot", "src", "config", "config.py"), + ]; + + for (const candidate of candidates) { + if (!existsSync(candidate)) { + continue; + } + + try { + const content = await readFile(candidate, "utf8"); + const match = content.match(/^\s*CONFIG_VERSION\s*:\s*str\s*=\s*["']([^"']+)["']/mu) + ?? content.match(/^\s*CONFIG_VERSION\s*=\s*["']([^"']+)["']/mu); + const version = match?.[1]?.trim(); + if (version) { + return version; + } + } catch { + // Try the next source, then fall back to the bundled-safe version. + } + } + + return MAIBOT_FALLBACK_CONFIG_VERSION; + } + + private async checkDependencies(): Promise { + const cached = this.dependencyCache; + if (cached && cached.expiresAt > Date.now()) { + return cached.checks; + } + + const checks: InitCheck[] = []; + const python = this.getPythonPath(); + const bundledPython = this.getBundledPythonPath(); + const pythonSource = bundledPython && samePath(bundledPython, python) ? "内置 Python" : "系统 Python"; + if (!existsSync(python)) { + checks.push({ + id: "python-dependencies", + label: "Python 依赖完整性", + status: "error", + detail: `未找到内置 Python 或系统 Python ${PYTHON_MINIMUM_VERSION}+,无法检查依赖`, + path: python, + actionLabel: "下载 Python", + actionUrl: PYTHON_DOWNLOAD_URL, + }); + } else { + try { + const output = await runProcess( + python, + [ + "-c", + [ + "import sys, ssl, sqlite3, tomllib", + `minimum = (${PYTHON_MINIMUM_VERSION.split(".").join(", ")})`, + "version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'", + `if sys.version_info < minimum: raise SystemExit(f'Python {version} is too old; need >= ${PYTHON_MINIMUM_VERSION}')`, + "print(f'Python {version}')", + ].join("\n"), + ], + this.paths.installRoot, + ); + checks.push({ + id: "python-runtime-smoke", + label: "Python 标准库", + status: "ok", + detail: output ? `${output} (${pythonSource})` : `${pythonSource} 可启动,ssl/sqlite3/tomllib 可导入`, + path: python, + }); + } catch (error) { + checks.push({ + id: "python-runtime-smoke", + label: "Python 标准库", + status: "error", + detail: `Python 版本或标准库不符合要求: ${toDetail(error)}`, + path: python, + actionLabel: "下载 Python", + actionUrl: PYTHON_DOWNLOAD_URL, + }); + } + + try { + const output = await runProcess(python, ["-m", "pip", "check"], this.paths.installRoot, 15_000); + checks.push({ + id: "python-pip-check", + label: "Python 包依赖", + status: "ok", + detail: output || "pip check 未发现损坏依赖", + path: python, + }); + } catch (error) { + const detail = toDetail(error); + checks.push( + isCleanPipCheckOutput(detail) + ? { + id: "python-pip-check", + label: "Python 包依赖", + status: "ok", + detail, + path: python, + } + : { + id: "python-pip-check", + label: "Python 包依赖", + status: "error", + detail: `pip 检查失败: ${detail}`, + path: python, + actionLabel: "下载 Python", + actionUrl: PYTHON_DOWNLOAD_URL, + }, + ); + } + } + + const git = this.getGitPath(); + const bundledGit = this.getBundledGitPath(); + const gitSource = bundledGit && samePath(bundledGit, git) ? "内置 Git" : "系统 Git"; + if (!existsSync(git)) { + checks.push({ + id: "git-runtime", + label: "Git", + status: "error", + detail: "未找到可用 Git", + path: git, + actionLabel: "下载 Git", + actionUrl: GIT_DOWNLOAD_URL, + }); + } else { + try { + const output = await runProcess(git, ["--version"], this.paths.installRoot); + checks.push({ + id: "git-runtime", + label: "Git", + status: "ok", + detail: output ? `${output} (${gitSource})` : `${gitSource} 可启动`, + path: git, + }); + } catch (error) { + checks.push({ + id: "git-runtime", + label: "Git", + status: "error", + detail: `Git 损坏或不可启动: ${toDetail(error)}`, + path: git, + actionLabel: "下载 Git", + actionUrl: GIT_DOWNLOAD_URL, + }); + } + } + + this.dependencyCache = { expiresAt: Date.now() + DEPENDENCY_CACHE_MS, checks }; + return checks; + } + + clearDependencyCache(): void { + this.dependencyCache = undefined; + } + + private getCachedDependencyChecks(): InitCheck[] { + if (this.dependencyCache) { + return this.dependencyCache.checks; + } + + return [ + { + id: "python-runtime-smoke", + label: "Python 标准库", + status: "warning", + detail: "后台检查中", + path: this.getPythonPath(), + }, + { + id: "python-pip-check", + label: "Python 包依赖", + status: "warning", + detail: "后台检查中", + path: this.getPythonPath(), + }, + { + id: "git-runtime", + label: "Git", + status: "warning", + detail: "后台检查中", + path: this.getGitPath(), + }, + ]; + } + + private async checkNapCatWebUi(): Promise { + const result = await this.readNapCatWebUiToken(); + if (result.token) { + return { + id: "napcat-webui-token", + label: "NapCat WebUI token", + status: "ok", + detail: "宸叉壘鍒?token", + }; + } + + if (result.exists) { + return { + id: "napcat-webui-token", + label: "NapCat WebUI token", + status: "error", + detail: result.error ?? "webui.json 存在但缺少 token", + }; + } + + return { + id: "napcat-webui-token", + label: "NapCat WebUI token", + status: "warning", + detail: "尚未创建,保存 QQ 或启动 NapCat 前会自动生成", + }; + } + + private async createNapCatConfigs( + qqAccount: string, + websocketToken: string, + websocketPort = NAPCAT_ADAPTER_PORT, + ): Promise { + const napcatProtocolConfig = { + enable: false, + network: { + httpServers: [], + websocketServers: [], + websocketClients: [], + }, + }; + const napcatConfig = { + fileLog: false, + consoleLog: true, + fileLogLevel: "debug", + consoleLogLevel: "info", + packetBackend: "auto", + packetServer: "", + o3HookMode: 1, + bypass: { + hook: false, + window: false, + module: false, + process: false, + container: false, + js: false, + }, + autoTimeSync: true, + }; + const onebotConfig = { + network: { + httpServers: [], + httpSseServers: [], + httpClients: [], + websocketServers: [ + { + enable: true, + name: "MaiBot Main", + host: "127.0.0.1", + port: websocketPort, + reportSelfMessage: false, + enableForcePushEvent: true, + messagePostFormat: "array", + token: websocketToken, + debug: false, + heartInterval: 30000, + }, + ], + websocketClients: [], + plugins: [], + }, + musicSignUrl: "", + enableLocalFile2Url: false, + parseMultMsg: false, + imageDownloadProxy: "", + timeout: { + baseTimeout: 10000, + uploadSpeedKBps: 256, + downloadSpeedKBps: 256, + maxTimeout: 1800000, + }, + }; + + for (const configDir of await this.findNapCatRuntimeConfigDirs()) { + await mkdir(configDir, { recursive: true }); + await writeFile( + join(configDir, `napcat_protocol_${qqAccount}.json`), + JSON.stringify(napcatProtocolConfig, null, 2), + "utf8", + ); + await writeFile( + join(configDir, `onebot11_${qqAccount}.json`), + JSON.stringify(onebotConfig, null, 2), + "utf8", + ); + await writeFile( + join(configDir, `napcat_${qqAccount}.json`), + JSON.stringify(napcatConfig, null, 2), + "utf8", + ); + } + } + + private async createSnowLumaConfigs( + qqAccount: string, + websocketToken: string, + websocketPort = SNOWLUMA_ONEBOT_PORT, + ): Promise { + const configDir = join(this.paths.snowlumaRoot, "config"); + await mkdir(configDir, { recursive: true }); + await writeFile( + join(configDir, "runtime.json"), + JSON.stringify({ webuiPort: SNOWLUMA_WEBUI_PORT, hookAutoLoad: false }, null, 2), + "utf8", + ); + const onebotConfig = { + networks: { + httpServers: [], + httpClients: [], + wsServers: [ + { + name: "MaiBot Main", + host: "127.0.0.1", + port: websocketPort, + path: "/", + role: "Universal", + accessToken: websocketToken, + messageFormat: "array", + reportSelfMessage: false, + }, + ], + wsClients: [], + }, + musicSignUrl: "", + }; + const serialized = JSON.stringify(onebotConfig, null, 2); + await writeFile(join(configDir, "onebot.json"), serialized, "utf8"); + await writeFile(join(configDir, `onebot_${qqAccount}.json`), serialized, "utf8"); + } + + private async findNapCatWebUiFiles(): Promise { + const configDirs = await this.findNapCatWebUiConfigDirs(); + return configDirs.map((configDir) => join(configDir, "webui.json")); + } + + private async findNapCatWebUiConfigDirs(): Promise { + return this.findNapCatRuntimeConfigDirs(); + } + + private async findNapCatRuntimeConfigDirs(): Promise { + const versions = await this.findNapCatVersions(); + return [ + join(this.paths.napcatRoot, "napcat", "config"), + ...versions.flatMap((version) => [ + join(this.paths.napcatRoot, "versions", version, "resources", "app", "napcat", "config"), + join( + this.napcatFrameworkRoot(), + "versions", + version, + "resources", + "app", + "LiteLoader", + "plugins", + "NapCat", + "config", + ), + ]), + ]; + } + + private async findNapCatVersions(): Promise { + const roots = [ + join(this.paths.napcatRoot, "versions"), + join(this.napcatFrameworkRoot(), "versions"), + join(this.paths.bundledModulesRoot, "napcat", "versions"), + join(this.paths.bundledModulesRoot, "napcatframework", "versions"), + ]; + const versions = new Set(); + + for (const root of roots) { + if (!existsSync(root)) { + continue; + } + + const entries = await readdir(root, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + versions.add(entry.name); + } + } + } + + return versions.size > 0 ? [...versions] : [NAPCAT_FALLBACK_VERSION]; + } + +} diff --git a/src/main/services/instance-lock.ts b/src/main/services/instance-lock.ts new file mode 100644 index 0000000..feb6b44 --- /dev/null +++ b/src/main/services/instance-lock.ts @@ -0,0 +1,103 @@ +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { RuntimePaths } from "../../shared/contracts"; + +interface LockPayload { + pid: number; + installRoot: string; + startedAt: number; +} + +export interface InstallInstanceLock { + acquired: boolean; + lockPath: string; + existing?: LockPayload; + release: () => void; +} + +function readLockPayload(lockPath: string): LockPayload | undefined { + try { + return JSON.parse(readFileSync(lockPath, "utf8")) as LockPayload; + } catch { + return undefined; + } +} + +function isProcessAlive(pid: number | undefined): boolean { + if (!pid || pid === process.pid) { + return false; + } + + try { + process.kill(pid, 0); + return true; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + return code === "EPERM"; + } +} + +export function acquireInstallInstanceLock(paths: RuntimePaths): InstallInstanceLock { + mkdirSync(paths.userDataRoot, { recursive: true }); + const lockPath = join(paths.userDataRoot, "instance.lock"); + const payload: LockPayload = { + pid: process.pid, + installRoot: paths.installRoot, + startedAt: Date.now(), + }; + + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + writeFileSync(lockPath, `${JSON.stringify(payload, null, 2)}\n`, { flag: "wx" }); + return { + acquired: true, + lockPath, + release: () => { + const current = readLockPayload(lockPath); + if (current?.pid === process.pid) { + try { + unlinkSync(lockPath); + } catch { + // The lock is best-effort; if it is already gone, shutdown can continue. + } + } + }, + }; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "EEXIST") { + throw error; + } + + const existing = readLockPayload(lockPath); + if (isProcessAlive(existing?.pid)) { + return { + acquired: false, + lockPath, + existing, + release: () => undefined, + }; + } + + if (existsSync(lockPath)) { + try { + unlinkSync(lockPath); + } catch { + return { + acquired: false, + lockPath, + existing, + release: () => undefined, + }; + } + } + } + } + + return { + acquired: false, + lockPath, + existing: readLockPayload(lockPath), + release: () => undefined, + }; +} diff --git a/src/main/services/local-chat-adapter.ts b/src/main/services/local-chat-adapter.ts new file mode 100644 index 0000000..c72242c --- /dev/null +++ b/src/main/services/local-chat-adapter.ts @@ -0,0 +1,845 @@ +import { EventEmitter } from "node:events"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import WebSocket from "ws"; +import type { + LocalChatConnectionState, + LocalChatConnectRequest, + LocalChatEvent, + LocalChatFileAttachment, + LocalChatImageAttachment, + LocalChatMessageEvent, + LocalChatMessageQuote, + LocalChatPlannerToolArgument, + LocalChatPlannerToolCall, + LocalChatSendRequest, + LocalChatVoiceAttachment, + RuntimePaths, +} from "../../shared/contracts"; + +const DEFAULT_WEBUI_ORIGIN = "http://127.0.0.1:8001"; +const DEFAULT_USER_ID = "onekey-local-user"; +const DEFAULT_WEBUI_USER_ID = `webui_user_${DEFAULT_USER_ID}`; +const DEFAULT_USER_NAME = "本地用户"; +const MESSAGE_HISTORY_LIMIT = 120; +const SESSION_ID = "desktop-simple-chat"; +const WS_REQUEST_TIMEOUT_MS = 8_000; +const REPLY_MESSAGE_PREFIX = /^\s*\[回复消息\]\s*/u; + +interface UnifiedWsEvent { + op?: unknown; + domain?: unknown; + event?: unknown; + session?: unknown; + data?: unknown; +} + +interface UnifiedWsResponse { + op?: unknown; + id?: unknown; + ok?: unknown; + data?: unknown; + error?: unknown; +} + +interface PendingRequest { + reject: (error: Error) => void; + resolve: (data: unknown) => void; + timeout: NodeJS.Timeout; +} + +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; +} + +function asString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function asNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function parseSocketPayload(data: WebSocket.RawData): unknown | undefined { + try { + const text = Array.isArray(data) + ? Buffer.concat(data).toString("utf8") + : Buffer.isBuffer(data) + ? data.toString("utf8") + : data.toString(); + return JSON.parse(text); + } catch { + return undefined; + } +} + +function normalizeTimestamp(value: unknown): number { + const timestamp = asNumber(value); + if (!timestamp) { + return Date.now(); + } + return timestamp > 10_000_000_000 ? Math.round(timestamp) : Math.round(timestamp * 1000); +} + +function imagePlaceholder(images: LocalChatImageAttachment[]): string { + if (images.length === 0) { + return ""; + } + return images.length === 1 ? "[图片]" : `[图片 x${images.length}]`; +} + +function emojiPlaceholder(emojis: LocalChatImageAttachment[]): string { + if (emojis.length === 0) { + return ""; + } + return emojis.length === 1 ? "[表情]" : `[表情 x${emojis.length}]`; +} + +function filePlaceholder(files: LocalChatFileAttachment[]): string { + return files.map((file) => `[文件] ${file.name}`).join("\n"); +} + +function voicePlaceholder(voices: LocalChatVoiceAttachment[]): string { + if (voices.length === 0) { + return ""; + } + return voices.length === 1 ? "[语音]" : `[语音 x${voices.length}]`; +} + +function imagePayload(images: LocalChatImageAttachment[]): Record[] { + return images.map((image) => ({ + name: image.name ?? "", + mime_type: image.mimeType, + base64: image.base64.trim(), + data_url: image.dataUrl ?? `data:${image.mimeType};base64,${image.base64.trim()}`, + size: image.size ?? 0, + })); +} + +function imageAttachments(value: unknown): LocalChatImageAttachment[] { + if (!Array.isArray(value)) { + return []; + } + return value.flatMap((item) => { + const record = asRecord(item); + if (!record) { + return []; + } + const mimeType = asString(record.mimeType) ?? asString(record.mime_type) ?? "image/png"; + const dataUrl = asString(record.dataUrl) ?? asString(record.data_url); + let base64 = asString(record.base64) ?? ""; + if (!base64 && dataUrl?.startsWith("data:image/") && dataUrl.includes(",")) { + base64 = dataUrl.split(",", 2)[1]?.trim() ?? ""; + } + if (!base64 || !mimeType.startsWith("image/")) { + return []; + } + return [{ + name: asString(record.name), + mimeType, + base64, + dataUrl: dataUrl ?? `data:${mimeType};base64,${base64}`, + size: asNumber(record.size), + }]; + }); +} + +function voicePayload(voices: LocalChatVoiceAttachment[]): Record[] { + return voices.map((voice) => ({ + name: voice.name ?? "", + mime_type: voice.mimeType, + base64: voice.base64.trim(), + data_url: voice.dataUrl ?? `data:${voice.mimeType};base64,${voice.base64.trim()}`, + size: voice.size ?? 0, + })); +} + +function voiceAttachments(value: unknown): LocalChatVoiceAttachment[] { + if (!Array.isArray(value)) { + return []; + } + return value.flatMap((item) => { + const record = asRecord(item); + if (!record) { + return []; + } + const mimeType = asString(record.mimeType) ?? asString(record.mime_type) ?? "audio/mpeg"; + const dataUrl = asString(record.dataUrl) ?? asString(record.data_url); + let base64 = asString(record.base64) ?? ""; + if (!base64 && dataUrl?.startsWith("data:audio/") && dataUrl.includes(",")) { + base64 = dataUrl.split(",", 2)[1]?.trim() ?? ""; + } + if (!base64 || !mimeType.startsWith("audio/")) { + return []; + } + return [{ + name: asString(record.name), + mimeType, + base64, + dataUrl: dataUrl ?? `data:${mimeType};base64,${base64}`, + size: asNumber(record.size), + }]; + }); +} + +function filePayload(files: LocalChatFileAttachment[]): Record[] { + return files.map((file) => ({ + name: file.name, + mime_type: file.mimeType, + base64: file.base64.trim(), + size: file.size, + })); +} + +function fileAttachments(value: unknown): LocalChatFileAttachment[] { + if (!Array.isArray(value)) { + return []; + } + return value.flatMap((item) => { + const record = asRecord(item); + if (!record) { + return []; + } + const name = asString(record.name) ?? asString(record.file_name) ?? asString(record.filename); + const base64 = asString(record.base64) ?? ""; + const size = asNumber(record.size) ?? 0; + if (!name || !base64) { + return []; + } + return [{ + name, + mimeType: asString(record.mimeType) ?? asString(record.mime_type) ?? "application/octet-stream", + base64, + size, + }]; + }); +} + +function plannerContent(data: Record): string { + const planner = asRecord(data.planner); + const content = asString(data.content) ?? asString(planner?.content); + return content ?? ""; +} + +function stringifyToolArguments(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + if (typeof value === "string") { + return value.trim() || undefined; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function stringifyToolValue(value: unknown): string { + if (value === undefined) { + return ""; + } + if (value === null) { + return "null"; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function parseToolArguments(value: unknown): LocalChatPlannerToolArgument[] | undefined { + let parsed = value; + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + try { + parsed = JSON.parse(trimmed); + } catch { + return [{ key: "参数", value: trimmed }]; + } + } + const record = asRecord(parsed); + if (!record) { + return parsed === undefined || parsed === null + ? undefined + : [{ key: "参数", value: stringifyToolValue(parsed) }]; + } + const entries = Object.entries(record) + .map(([key, entryValue]) => ({ key, value: stringifyToolValue(entryValue) })) + .filter((entry) => entry.value.length > 0); + return entries.length ? entries : undefined; +} + +function plannerTools(data: Record): LocalChatPlannerToolCall[] { + const planner = asRecord(data.planner); + const rawToolCalls = Array.isArray(data.tool_calls) + ? data.tool_calls + : Array.isArray(planner?.tool_calls) + ? planner.tool_calls + : []; + const calls = rawToolCalls + .map((item): LocalChatPlannerToolCall | undefined => { + const record = asRecord(item); + const name = asString(record?.name); + if (!record || !name) { + return undefined; + } + return { + id: asString(record.id), + name, + arguments: parseToolArguments(record.arguments ?? record.arguments_raw), + argumentsText: stringifyToolArguments(record.arguments ?? record.arguments_raw), + }; + }) + .filter((item): item is LocalChatPlannerToolCall => Boolean(item)); + + const rawResults = Array.isArray(data.tools) ? data.tools : []; + for (const item of rawResults) { + const record = asRecord(item); + const name = asString(record?.tool_name); + if (!record || !name) { + continue; + } + const id = asString(record.tool_call_id); + const existing = calls.find((call) => (id && call.id === id) || call.name === name); + const result = { + id, + name, + arguments: parseToolArguments(record.tool_args), + argumentsText: stringifyToolArguments(record.tool_args), + resultText: asString(record.summary) ?? stringifyToolArguments(record.detail), + success: typeof record.success === "boolean" ? record.success : undefined, + durationMs: asNumber(record.duration_ms), + }; + if (existing) { + Object.assign(existing, result); + } else { + calls.push(result); + } + } + + return calls; +} + +function quoteFromRecord(record: Record | undefined): LocalChatMessageQuote | undefined { + if (!record) { + return undefined; + } + const quoteRecord = asRecord(record.quote) + ?? asRecord(record.reply) + ?? asRecord(record.replied_message) + ?? asRecord(record.quote_message) + ?? asRecord(record.reference); + const content = asString(quoteRecord?.content) + ?? asString(quoteRecord?.text) + ?? asString(quoteRecord?.message) + ?? asString(record.quote_content) + ?? asString(record.reply_content); + if (!content) { + return undefined; + } + const sender = asRecord(quoteRecord?.sender); + return { + messageId: asString(quoteRecord?.message_id) ?? asString(quoteRecord?.id) ?? asString(record.quote_message_id), + sender: asString(sender?.name) + ?? asString(quoteRecord?.sender_name) + ?? asString(quoteRecord?.sender) + ?? asString(record.quote_sender_name), + content, + }; +} + +function splitReplyMessage(content: string): { content: string; hasReplyPrefix: boolean } { + if (!REPLY_MESSAGE_PREFIX.test(content)) { + return { content, hasReplyPrefix: false }; + } + return { + content: content.replace(REPLY_MESSAGE_PREFIX, "").trim(), + hasReplyPrefix: true, + }; +} + +function historyMessageToLocal(message: Record): LocalChatMessageEvent | undefined { + const rawContent = asString(message.content); + const images = imageAttachments(message.images); + const emojis = imageAttachments(message.emojis); + const files = fileAttachments(message.files); + const voices = voiceAttachments(message.voices); + const fallbackContent = [imagePlaceholder(images), emojiPlaceholder(emojis), voicePlaceholder(voices), filePlaceholder(files)] + .filter(Boolean) + .join("\n"); + if (!rawContent && !fallbackContent) { + return undefined; + } + + const parsed = splitReplyMessage(rawContent ?? fallbackContent); + const type = asString(message.type); + const isBot = message.is_bot === true || type === "bot"; + return { + id: asString(message.id) ?? `history-${Date.now()}-${Math.random().toString(16).slice(2)}`, + role: isBot ? "bot" : "user", + content: parsed.content, + timestamp: normalizeTimestamp(message.timestamp), + sender: asString(message.sender_name) ?? (isBot ? "MaiBot" : DEFAULT_USER_NAME), + images, + emojis, + files, + voices, + quote: quoteFromRecord(message), + }; +} + +export class LocalChatAdapter extends EventEmitter { + private socket: WebSocket | null = null; + private state: LocalChatConnectionState = "idle"; + private currentUrl = ""; + private connectingPromise: Promise | null = null; + private messages: LocalChatMessageEvent[] = []; + private pendingRequests = new Map(); + private requestCounter = 0; + private lastUserName = DEFAULT_USER_NAME; + private runtimeSessionId: string | null = null; + private monitorSessionId: string | null = null; + + constructor(private readonly paths: RuntimePaths) { + super(); + } + + getState(): LocalChatConnectionState { + return this.state; + } + + listMessages(): LocalChatMessageEvent[] { + return [...this.messages]; + } + + async connect(_request?: LocalChatConnectRequest): Promise { + if (this.socket?.readyState === WebSocket.OPEN) { + return this.state; + } + if (this.connectingPromise) { + await this.connectingPromise; + return this.state; + } + + this.connectingPromise = this.openSocket().finally(() => { + this.connectingPromise = null; + }); + await this.connectingPromise; + return this.state; + } + + disconnect(): void { + const socket = this.socket; + this.socket = null; + this.runtimeSessionId = null; + this.monitorSessionId = null; + this.rejectPendingRequests(new Error("简单聊聊连接已关闭")); + if (socket) { + socket.removeAllListeners(); + if (socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.OPEN) { + socket.close(); + } + socket.terminate(); + } + this.setState("idle"); + } + + async send(request: LocalChatSendRequest): Promise { + await this.connect(); + const socket = this.socket; + if (!socket || socket.readyState !== WebSocket.OPEN) { + throw new Error("简单聊聊未连接"); + } + + const content = request.content.trim(); + const images = (request.images ?? []).filter((image) => image.base64.trim() && image.mimeType.startsWith("image/")); + const emojis = (request.emojis ?? []).filter((emoji) => emoji.base64.trim() && emoji.mimeType.startsWith("image/")); + const files = (request.files ?? []).filter((file) => file.base64.trim() && file.name.trim()); + const voices = (request.voices ?? []).filter((voice) => voice.base64.trim() && voice.mimeType.startsWith("audio/")); + if (!content && images.length === 0 && emojis.length === 0 && files.length === 0 && voices.length === 0) { + throw new Error("消息内容为空"); + } + + const displayContent = [content, imagePlaceholder(images), emojiPlaceholder(emojis), voicePlaceholder(voices), filePlaceholder(files)] + .filter(Boolean) + .join("\n"); + this.lastUserName = request.userName?.trim() || DEFAULT_USER_NAME; + const message: LocalChatMessageEvent = { + id: `local-${Date.now()}-${Math.random().toString(16).slice(2)}`, + role: "user", + content: displayContent, + timestamp: Date.now(), + sender: this.lastUserName, + images, + emojis, + files, + voices, + }; + + await this.sendRequest({ + op: "call", + domain: "chat", + method: "message.send", + session: SESSION_ID, + data: { + content, + images: imagePayload(images), + emojis: imagePayload(emojis), + files: filePayload(files), + voices: voicePayload(voices), + user_name: this.lastUserName, + }, + }); + this.emitMessage(message); + return message; + } + + dispose(): void { + this.disconnect(); + this.removeAllListeners(); + } + + onEvent(callback: (event: LocalChatEvent) => void): () => void { + this.on("event", callback); + return () => this.off("event", callback); + } + + private async openSocket(): Promise { + const origin = await this.readWebUiOrigin(); + const token = await this.readWebUiToken(); + const wsOrigin = origin.replace(/^http/u, "ws").replace(/\/+$/u, ""); + this.currentUrl = `${wsOrigin}/api/webui/ws`; + this.setState("connecting"); + + await new Promise((resolve, reject) => { + const socket = new WebSocket(this.currentUrl, { + headers: token ? { Cookie: `maibot_session=${encodeURIComponent(token)}` } : {}, + }); + let settled = false; + const timeout = setTimeout(() => { + finish(new Error(`连接 MaiBot 简单聊聊超时:${origin}`)); + }, WS_REQUEST_TIMEOUT_MS); + + const finish = (error?: Error): void => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeout); + if (error) { + socket.close(); + reject(error); + return; + } + resolve(); + }; + + socket.on("open", () => { + this.socket = socket; + this.setState("connected"); + void this.initializeSession().then(() => finish()).catch(finish); + }); + socket.on("message", (data) => this.handleSocketMessage(data)); + socket.on("error", () => { + this.setState("error"); + finish(new Error(`无法连接 MaiBot 简单聊聊:${origin}`)); + }); + socket.on("close", () => { + this.rejectPendingRequests(new Error("简单聊聊连接已断开")); + if (this.socket === socket) { + this.socket = null; + this.setState(this.state === "idle" ? "idle" : "error"); + } + }); + }); + } + + private async initializeSession(): Promise { + this.monitorSessionId = null; + const response = asRecord(await this.sendRequest({ + op: "call", + domain: "chat", + method: "session.open", + session: SESSION_ID, + data: { + user_id: DEFAULT_USER_ID, + user_name: this.lastUserName, + platform: "webui", + restore: true, + }, + })); + this.runtimeSessionId = asString(response?.session_id) ?? null; + await this.sendRequest({ + op: "subscribe", + domain: "maisaka_monitor", + topic: "main", + }); + } + + private async readWebUiOrigin(): Promise { + const candidates = [ + join(this.paths.maibotRoot, "data", "webui.json"), + join(this.paths.bundledModulesRoot, "MaiBot", "data", "webui.json"), + ]; + for (const configPath of candidates) { + try { + const raw = JSON.parse(await readFile(configPath, "utf8")) as Record; + const port = asNumber(raw.webui_port) ?? asNumber(raw.port); + if (port) { + return `http://127.0.0.1:${port}`; + } + } catch { + // Try the next known location. + } + } + return DEFAULT_WEBUI_ORIGIN; + } + + private async readWebUiToken(): Promise { + const candidates = [ + join(this.paths.maibotRoot, "data", "webui.json"), + join(this.paths.bundledModulesRoot, "MaiBot", "data", "webui.json"), + ]; + for (const configPath of candidates) { + try { + const raw = JSON.parse(await readFile(configPath, "utf8")) as Record; + const token = asString(raw.access_token); + if (token) { + return token; + } + } catch { + // Try the next known location. + } + } + return null; + } + + private sendRequest(payload: Record): Promise { + const socket = this.socket; + if (!socket || socket.readyState !== WebSocket.OPEN) { + return Promise.reject(new Error("简单聊聊未连接")); + } + + this.requestCounter += 1; + const id = `onekey-${Date.now()}-${this.requestCounter}`; + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.pendingRequests.delete(id); + reject(new Error("简单聊聊插件请求超时")); + }, WS_REQUEST_TIMEOUT_MS); + this.pendingRequests.set(id, { resolve, reject, timeout }); + socket.send(JSON.stringify({ ...payload, id })); + }); + } + + private rejectPendingRequests(error: Error): void { + this.pendingRequests.forEach((pending) => { + clearTimeout(pending.timeout); + pending.reject(error); + }); + this.pendingRequests.clear(); + } + + private handleSocketMessage(data: WebSocket.RawData): void { + const payload = parseSocketPayload(data); + const record = asRecord(payload); + if (!record) { + return; + } + + if (record.op === "response") { + this.handleResponse(record as UnifiedWsResponse); + return; + } + if (record.op === "event") { + this.handleEvent(record as UnifiedWsEvent); + } + } + + private handleResponse(response: UnifiedWsResponse): void { + const id = asString(response.id); + if (!id) { + return; + } + const pending = this.pendingRequests.get(id); + if (!pending) { + return; + } + + clearTimeout(pending.timeout); + this.pendingRequests.delete(id); + if (response.ok === true) { + pending.resolve(response.data); + return; + } + + const error = asRecord(response.error); + pending.reject(new Error(asString(error?.message) ?? "简单聊聊插件请求失败")); + } + + private handleEvent(event: UnifiedWsEvent): void { + const domain = asString(event.domain); + const eventName = asString(event.event); + const data = asRecord(event.data); + if (!domain || !eventName || !data) { + return; + } + + if (domain === "chat" && event.session === SESSION_ID) { + this.handleChatEvent(eventName, data); + return; + } + if (domain === "maisaka_monitor" && this.isLocalPlannerEvent(data)) { + this.handlePlannerEvent(eventName, data); + } + } + + private isLocalPlannerEvent(data: Record): boolean { + const sessionId = asString(data.session_id); + if (!sessionId) { + return false; + } + if (this.runtimeSessionId === sessionId || this.monitorSessionId === sessionId) { + return true; + } + + const platform = asString(data.platform); + const userId = asString(data.user_id); + const groupId = asString(data.group_id); + const isGroupChat = data.is_group_chat === true; + if (platform === "webui" && userId === DEFAULT_WEBUI_USER_ID && !isGroupChat && !groupId) { + this.monitorSessionId = sessionId; + return true; + } + + return false; + } + + private handleChatEvent(eventName: string, data: Record): void { + if (eventName === "typing" || eventName === "pong" || eventName === "virtual_identity_set") { + return; + } + + if (eventName === "history") { + const history = Array.isArray(data.messages) ? data.messages : []; + for (const item of history) { + const message = historyMessageToLocal(asRecord(item) ?? {}); + if (message) { + this.emitMessage(message); + } + } + return; + } + + const images = imageAttachments(data.images); + const emojis = imageAttachments(data.emojis); + const files = fileAttachments(data.files); + const voices = voiceAttachments(data.voices); + const rawContent = asString(data.content); + const fallbackContent = [imagePlaceholder(images), emojiPlaceholder(emojis), voicePlaceholder(voices), filePlaceholder(files)] + .filter(Boolean) + .join("\n"); + if (!rawContent && !fallbackContent) { + return; + } + + const sender = asRecord(data.sender); + const isUser = eventName === "user_message" || sender?.is_bot === false; + const role = eventName === "error" ? "error" : isUser ? "user" : eventName === "system" ? "system" : "bot"; + const parsed = splitReplyMessage(rawContent ?? fallbackContent); + const content = parsed.content; + if ( + role === "user" + && this.messages.some((message) => + message.role === "user" + && message.content === content + && Date.now() - message.timestamp < 10_000 + ) + ) { + return; + } + this.emitMessage({ + id: asString(data.message_id) ?? `${eventName}-${Date.now()}-${Math.random().toString(16).slice(2)}`, + role, + content, + timestamp: normalizeTimestamp(data.timestamp), + sender: asString(sender?.name) ?? (role === "bot" ? "MaiBot" : undefined), + images, + emojis, + files, + voices, + quote: this.localQuoteForMessage(data, parsed.hasReplyPrefix), + }); + } + + private localQuoteForMessage(data: Record, hasReplyPrefix: boolean): LocalChatMessageQuote | undefined { + const explicitQuote = quoteFromRecord(data); + if (explicitQuote) { + return explicitQuote; + } + if (!hasReplyPrefix) { + return undefined; + } + const latestUserMessage = [...this.messages].reverse().find((message) => message.role === "user" && message.content.trim()); + if (!latestUserMessage) { + return undefined; + } + return { + messageId: latestUserMessage.id, + sender: latestUserMessage.sender, + content: latestUserMessage.content, + }; + } + + private handlePlannerEvent(eventName: string, data: Record): void { + if (eventName !== "planner.response" && eventName !== "planner.finalized") { + return; + } + + const content = plannerContent(data); + if (!content) { + return; + } + + this.emitMessage({ + id: `planner-${asString(data.session_id) ?? "session"}-${asString(data.cycle_id) ?? Date.now().toString()}`, + role: "system", + content, + timestamp: normalizeTimestamp(data.timestamp), + sender: "MaiSaka Planner", + kind: "planner", + final: eventName === "planner.finalized", + plannerTools: plannerTools(data), + }); + } + + private emitMessage(message: LocalChatMessageEvent): void { + const existingIndex = this.messages.findIndex((item) => item.id === message.id); + if (existingIndex >= 0) { + this.messages = this.messages.map((item, index) => index === existingIndex ? { ...item, ...message } : item); + } else { + this.messages = [...this.messages, message].slice(-MESSAGE_HISTORY_LIMIT); + } + this.emitEvent(message); + } + + private setState(state: LocalChatConnectionState): void { + this.state = state; + this.emitEvent({ type: "state", state, url: this.currentUrl }); + } + + private emitEvent(event: LocalChatEvent): void { + this.emit("event", event); + } +} diff --git a/src/main/services/log-store.ts b/src/main/services/log-store.ts new file mode 100644 index 0000000..ee0f107 --- /dev/null +++ b/src/main/services/log-store.ts @@ -0,0 +1,61 @@ +import { EventEmitter } from "node:events"; +import { mkdir, appendFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogEntry, LogSource, LogStream, RuntimePaths } from "../../shared/contracts"; + +const MAX_BUFFERED_LOGS = 1000; + +function formatLogLine(entry: LogEntry): string { + const timestamp = new Date(entry.timestamp).toISOString(); + return `[${timestamp}] [${entry.stream}] ${entry.message}\n`; +} + +export class LogStore extends EventEmitter { + private readonly entries: LogEntry[] = []; + + constructor(private readonly paths: RuntimePaths) { + super(); + } + + append(source: LogSource, stream: LogStream, message: string): LogEntry { + const entry: LogEntry = { + id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, + source, + stream, + message: message.replace(/\r?\n$/, ""), + timestamp: Date.now(), + }; + + this.entries.push(entry); + if (this.entries.length > MAX_BUFFERED_LOGS) { + this.entries.splice(0, this.entries.length - MAX_BUFFERED_LOGS); + } + + this.writeEntry(entry); + this.emit("entry", entry); + return entry; + } + + list(): LogEntry[] { + return [...this.entries]; + } + + clear(): void { + this.entries.length = 0; + } + + getServiceLogPath(source: LogSource): string { + return join(this.paths.logsRoot, `${source}.log`); + } + + onEntry(callback: (entry: LogEntry) => void): () => void { + this.on("entry", callback); + return () => this.off("entry", callback); + } + + private writeEntry(entry: LogEntry): void { + void mkdir(this.paths.logsRoot, { recursive: true }) + .then(() => appendFile(this.getServiceLogPath(entry.source), formatLogLine(entry), "utf8")) + .catch(() => undefined); + } +} diff --git a/src/main/services/maibot-plugin-client.ts b/src/main/services/maibot-plugin-client.ts new file mode 100644 index 0000000..ddf5172 --- /dev/null +++ b/src/main/services/maibot-plugin-client.ts @@ -0,0 +1,1465 @@ +import { execFile } from "node:child_process"; +import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; +import type { + MaiBotPluginConfigSaveResult, + MaiBotPluginConfigSchema, + MaiBotPluginConfigState, + MaiBotPluginConfigValue, + MaiBotPluginConfigLocalizedText, + MaiBotInstalledPlugin, + MaiBotPluginListOptions, + MaiBotMarketPlugin, + MaiBotPluginListResult, + MaiBotPluginManifest, + MaiBotPluginOperationResult, + MaiBotPluginReadmeResult, + MaiBotPluginStats, + ModuleSourceConfig, +} from "../../shared/contracts"; + +const MARKET_URL = + "https://raw.githubusercontent.com/Mai-with-u/plugin-repo/main/plugin_details.json"; +const OFFICIAL_GITHUB_BASE_URL = "https://github.com/"; +const OFFICIAL_RAW_GITHUB_BASE_URL = "https://raw.githubusercontent.com/"; +const OFFICIAL_MAIBOT_REMOTE_URL = "https://github.com/Mai-with-u/MaiBot.git"; +const PLUGIN_STATS_URL = process.env.MAIBOT_PLUGIN_STATS_BASE_URL + ? `${process.env.MAIBOT_PLUGIN_STATS_BASE_URL.replace(/\/+$/u, "")}/stats/summary` + : "http://hyybuth.xyz:10059/stats/summary"; +const PLUGIN_STATS_BASE_URL = process.env.MAIBOT_PLUGIN_STATS_BASE_URL?.replace(/\/+$/u, "") ?? "http://hyybuth.xyz:10059"; +const MARKET_TIMEOUT_MS = 10_000; +const MAIBOT_API_TIMEOUT_MS = 3_000; +const PLUGIN_MARKET_CACHE_TTL_MS = 5 * 60 * 1000; +const PLUGIN_CONFIG_FILE = "config.toml"; + +export interface MaiBotPluginClientOptions { + maibotRoot: string; + gitPath: string; + getModuleSourceConfig?: () => Promise; +} + +interface GitRunResult { + exitCode: number; + output: string; +} + +interface CacheFile { + timestamp: number; + data: T; +} + +export class MaiBotPluginClient { + private readonly maibotRoot: string; + + private readonly pluginsRoot: string; + + private readonly gitPath: string; + + private readonly getModuleSourceConfig?: () => Promise; + + private marketCache: CacheFile | null = null; + + private marketRequest: Promise | null = null; + + private statsCache: CacheFile> | null = null; + + private statsRequest: Promise> | null = null; + + constructor(options: MaiBotPluginClientOptions) { + this.maibotRoot = resolve(options.maibotRoot); + this.pluginsRoot = resolve(this.maibotRoot, "plugins"); + this.gitPath = options.gitPath; + this.getModuleSourceConfig = options.getModuleSourceConfig; + } + + async listInstalled(serviceUrl?: string): Promise { + const runtimePlugins = await this.listRuntimeInstalled(serviceUrl); + if (runtimePlugins) { + return runtimePlugins; + } + + await mkdir(this.pluginsRoot, { recursive: true }); + const entries = await import("node:fs/promises").then(({ readdir }) => + readdir(this.pluginsRoot, { withFileTypes: true }), + ); + const plugins: MaiBotInstalledPlugin[] = []; + const seenIds = new Set(); + + for (const entry of entries) { + if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("__")) { + continue; + } + + const pluginPath = this.safePluginPath(entry.name, false); + const manifest = await this.readManifest(pluginPath); + if (!manifest?.name || !manifest.version) { + continue; + } + + const id = inferPluginId(entry.name, manifest); + if (seenIds.has(id)) { + continue; + } + seenIds.add(id); + const config = await this.readPluginConfig(pluginPath).catch(() => ({})); + const enabled = readPluginEnabled(config); + + plugins.push({ + id, + manifest: { ...manifest, id }, + path: pluginPath, + enabled, + loaded: undefined, + load_status: enabled ? "inactive" : "disabled", + }); + } + + return plugins.sort((left, right) => pluginName(left).localeCompare(pluginName(right), "zh-CN")); + } + + async listMarket(serviceUrl?: string, options: MaiBotPluginListOptions = {}): Promise { + const installed = await this.listInstalled(serviceUrl); + const installedById = new Map(installed.map((plugin) => [plugin.id, plugin])); + const [sourceList, stats] = await Promise.all([ + this.getMarketPlugins(options), + this.getPluginStatsSummary(options).catch(() => ({})), + ]); + const market = sourceList + .map((plugin) => { + const installedPlugin = installedById.get(plugin.id); + const statsItem = resolvePluginStats(plugin, stats); + return { + ...plugin, + installed: Boolean(installedPlugin), + installedVersion: installedPlugin ? pluginVersion(installedPlugin.manifest) : undefined, + downloads: statsItem?.downloads ?? plugin.downloads, + rating: statsItem?.rating ?? plugin.rating, + likes: statsItem?.likes ?? plugin.likes, + }; + }); + + return { installed, market, stats }; + } + + async install(pluginId: string, repositoryUrl: string, branch = "main"): Promise { + const targetPath = this.installTargetPath(pluginId); + if (await pathExists(targetPath)) { + throw new Error("鎻掍欢宸插畨瑁咃紝璇峰厛鍗歌浇"); + } + + await this.cloneRepository(await this.resolveSourceUrl(repositoryUrl), targetPath, branch); + const manifest = await this.validateInstalledManifest(targetPath, pluginId); + return { + success: true, + message: "鎻掍欢瀹夎鎴愬姛", + plugin_id: pluginId, + plugin_name: pluginName({ id: pluginId, manifest }), + new_version: pluginVersion(manifest), + }; + } + + async update( + pluginId: string, + repositoryUrl: string, + branch = "main", + latestVersion?: string, + ): Promise { + const pluginPath = await this.resolveInstalledPluginPath(pluginId); + if (!pluginPath) { + throw new Error("鎻掍欢鏈畨瑁咃紝璇峰厛瀹夎"); + } + + const oldManifest = await this.readManifest(pluginPath); + const oldVersion = oldManifest ? pluginVersion(oldManifest) : "unknown"; + if (latestVersion && !isNewerVersion(latestVersion, oldVersion)) { + throw new Error("褰撳墠宸叉槸鏈€鏂扮増鏈紝鏃犻渶鏇存柊"); + } + const beforeCommit = await this.currentGitCommit(pluginPath); + if (!beforeCommit) { + throw new Error("插件目录不是可更新的 Git 仓库,无法执行强制 pull"); + } + + try { + await this.forcePullRepository(pluginPath, await this.resolveSourceUrl(repositoryUrl), branch); + const newManifest = await this.validateInstalledManifest(pluginPath, pluginId, false); + return { + success: true, + message: "插件更新成功", + plugin_id: pluginId, + plugin_name: pluginName({ id: pluginId, manifest: newManifest }), + old_version: oldVersion, + new_version: pluginVersion(newManifest), + }; + } catch (error) { + await this.rollbackRepository(pluginPath, beforeCommit); + throw error; + } + } + + async uninstall(pluginId: string): Promise { + const pluginPath = await this.resolveInstalledPluginPath(pluginId); + if (!pluginPath) { + throw new Error("插件未安装"); + } + + const manifest = await this.readManifest(pluginPath); + await this.removePluginPath(pluginPath); + return { + success: true, + message: "鎻掍欢鍗歌浇鎴愬姛", + plugin_id: pluginId, + plugin_name: manifest ? pluginName({ id: pluginId, manifest }) : pluginId, + }; + } + + async getConfig(pluginId: string, serviceUrl?: string): Promise { + const pluginPath = await this.requireInstalledPluginPath(pluginId); + const configPath = resolve(pluginPath, PLUGIN_CONFIG_FILE); + if (!isPathInside(pluginPath, configPath)) { + throw new Error("鎻掍欢閰嶇疆璺緞瓒呭嚭鍏佽鑼冨洿"); + } + + const runtimeConfig = await this.getRuntimeConfig(pluginId, pluginPath, configPath, serviceUrl); + if (runtimeConfig) { + return runtimeConfig; + } + + const exists = await pathExists(configPath); + const raw = exists ? await readFile(configPath, "utf8") : ""; + const config = exists ? parsePluginConfig(raw, configPath) : {}; + + return { + pluginId, + pluginPath, + configPath, + exists, + config, + schema: buildPluginConfigSchema(config, "local"), + raw, + }; + } + + async saveConfig( + pluginId: string, + config: Record, + serviceUrl?: string, + ): Promise { + const pluginPath = await this.requireInstalledPluginPath(pluginId); + const configPath = resolve(pluginPath, PLUGIN_CONFIG_FILE); + if (!isPathInside(pluginPath, configPath)) { + throw new Error("鎻掍欢閰嶇疆璺緞瓒呭嚭鍏佽鑼冨洿"); + } + + const runtimeConfig = normalizePluginConfigRoot(config); + const runtimeResult = await this.saveRuntimeConfig(pluginId, runtimeConfig, pluginPath, configPath, serviceUrl); + if (runtimeResult) { + return runtimeResult; + } + + const normalizedConfig = normalizePluginConfigRootForToml(config); + let backupPath: string | undefined; + if (await pathExists(configPath)) { + backupPath = `${configPath}.${new Date().toISOString().replace(/[:.]/gu, "-")}.bak`; + await copyFile(configPath, backupPath); + } + + const raw = `${stringifyToml(normalizedConfig)}\n`; + await mkdir(pluginPath, { recursive: true }); + await writeFile(configPath, raw, "utf8"); + + return { + pluginId, + configPath, + config: normalizedConfig, + schema: buildPluginConfigSchema(normalizedConfig, "local"), + raw, + backupPath, + savedAt: Date.now(), + }; + } + + async getReadme(pluginId: string, repositoryUrl?: string): Promise { + const pluginPath = await this.resolveInstalledPluginPath(pluginId); + if (pluginPath) { + for (const readmeName of ["README.md", "readme.md", "Readme.md", "README.MD"]) { + const readmePath = resolve(pluginPath, readmeName); + if (!isPathInside(pluginPath, readmePath) || !(await pathExists(readmePath))) { + continue; + } + try { + return { success: true, content: await readFile(readmePath, "utf8") }; + } catch { + // Try the next known README casing. + } + } + } + + const remoteUrl = repositoryUrl ? githubRawReadmeUrl(await this.resolveSourceUrl(repositoryUrl)) : undefined; + if (!remoteUrl) { + return { success: false, error: "鏈壘鍒版彃浠?README" }; + } + + for (const branch of ["main", "master"]) { + const response = await fetchWithTimeout(remoteUrl(branch)).catch(() => null); + if (response?.ok) { + return { success: true, content: await response.text() }; + } + } + return { success: false, error: "鏈壘鍒版彃浠?README" }; + } + + async getStats(pluginId: string): Promise { + const response = await fetchWithTimeout(`${PLUGIN_STATS_BASE_URL}/stats/${encodeURIComponent(pluginId)}`).catch(() => null); + if (!response?.ok) { + return null; + } + const data = (await response.json()) as unknown; + return normalizePluginStatsDetail(pluginId, data); + } + + private installTargetPath(pluginId: string): string { + return this.safePluginPath(validatePluginId(pluginId).replace(/\./gu, "_"), false); + } + + private async resolveInstalledPluginPath(pluginId: string): Promise { + const normalizedId = validatePluginId(pluginId); + const directCandidates = [ + this.safePluginPath(normalizedId.replace(/\./gu, "_"), false), + this.safePluginPath(normalizedId, false), + ]; + + for (const candidate of directCandidates) { + if (await isDirectory(candidate)) { + return candidate; + } + } + + const installed = await this.listInstalled(); + return installed.find((plugin) => plugin.id === normalizedId)?.path ?? null; + } + + private async requireInstalledPluginPath(pluginId: string): Promise { + const pluginPath = await this.resolveInstalledPluginPath(pluginId); + if (!pluginPath) { + throw new Error("插件未安装"); + } + return pluginPath; + } + + private async readPluginConfig(pluginPath: string): Promise> { + const configPath = resolve(pluginPath, PLUGIN_CONFIG_FILE); + if (!isPathInside(pluginPath, configPath) || !(await pathExists(configPath))) { + return {}; + } + return parsePluginConfig(await readFile(configPath, "utf8"), configPath); + } + + private async getMarketPlugins(options: MaiBotPluginListOptions): Promise { + const cached = await this.readCache( + "onekey-plugin-market-list-cache.json", + this.marketCache, + isMarketPluginList, + ); + if (!options.forceRefresh && cached && Date.now() - cached.timestamp < PLUGIN_MARKET_CACHE_TTL_MS) { + this.marketCache = cached; + return cached.data; + } + + if (!this.marketRequest || options.forceRefresh) { + this.marketRequest = this.resolveSourceUrl(MARKET_URL) + .then((marketUrl) => fetchMarketPluginsUncached(marketUrl)) + .then(async (plugins) => { + const nextCache = { timestamp: Date.now(), data: plugins }; + this.marketCache = nextCache; + await this.writeCache("onekey-plugin-market-list-cache.json", nextCache); + return plugins; + }) + .catch((error) => { + if (cached) { + return cached.data; + } + throw error; + }) + .finally(() => { + this.marketRequest = null; + }); + } + + return this.marketRequest; + } + + private async getPluginStatsSummary(options: MaiBotPluginListOptions): Promise> { + const cached = await this.readCache( + "onekey-plugin-market-stats-cache.json", + this.statsCache, + isPluginStatsMap, + ); + if (!options.forceRefresh && cached && Date.now() - cached.timestamp < PLUGIN_MARKET_CACHE_TTL_MS) { + this.statsCache = cached; + return cached.data; + } + + if (!this.statsRequest || options.forceRefresh) { + this.statsRequest = fetchPluginStatsSummary() + .then(async (stats) => { + const nextCache = { timestamp: Date.now(), data: stats }; + this.statsCache = nextCache; + await this.writeCache("onekey-plugin-market-stats-cache.json", nextCache); + return stats; + }) + .catch((error) => { + if (cached) { + return cached.data; + } + throw error; + }) + .finally(() => { + this.statsRequest = null; + }); + } + + return this.statsRequest; + } + + private async resolveSourceUrl(url: string): Promise { + if (!this.getModuleSourceConfig) { + return url; + } + + try { + return rewriteGithubUrl(url, (await this.getModuleSourceConfig()).maibotUrl); + } catch { + return url; + } + } + + private async readCache( + fileName: string, + memoryCache: CacheFile | null, + validate: (value: unknown) => value is T, + ): Promise | null> { + if (memoryCache) { + return memoryCache; + } + + const cachePath = this.cachePath(fileName); + try { + const raw = JSON.parse(await readFile(cachePath, "utf8")) as Partial>; + if (typeof raw.timestamp !== "number" || !validate(raw.data)) { + return null; + } + return { timestamp: raw.timestamp, data: raw.data }; + } catch { + return null; + } + } + + private async writeCache(fileName: string, cache: CacheFile): Promise { + const cachePath = this.cachePath(fileName); + await mkdir(resolve(this.maibotRoot, "data"), { recursive: true }); + await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, "utf8").catch(() => undefined); + } + + private cachePath(fileName: string): string { + const cachePath = resolve(this.maibotRoot, "data", fileName); + if (!isPathInside(this.maibotRoot, cachePath)) { + throw new Error("鎻掍欢甯傚満缂撳瓨璺緞瓒呭嚭鍏佽鑼冨洿"); + } + return cachePath; + } + + private async cloneRepository(repositoryUrl: string, targetPath: string, branch: string): Promise { + await mkdir(this.pluginsRoot, { recursive: true }); + const args = ["clone", "--depth", "1", "--branch", branch || "main", repositoryUrl, targetPath]; + const result = await runGit(this.gitPath, args, this.maibotRoot); + if (result.exitCode !== 0) { + await rm(targetPath, { recursive: true, force: true }).catch(() => undefined); + throw new Error(result.output || "鍏嬮殕浠撳簱澶辫触"); + } + } + + private async currentGitCommit(pluginPath: string): Promise { + if (!(await isDirectory(join(pluginPath, ".git")))) { + return null; + } + const result = await runGit(this.gitPath, ["rev-parse", "HEAD"], pluginPath); + return result.exitCode === 0 && result.output.trim() ? result.output.trim().split(/\s+/u)[0] : null; + } + + private async forcePullRepository(pluginPath: string, repositoryUrl: string, branch: string): Promise { + const remote = repositoryUrl.trim(); + const targetBranch = branch || "main"; + await this.runGitOrThrow(pluginPath, ["remote", "set-url", "origin", remote], "更新插件远端失败"); + await this.runGitOrThrow(pluginPath, ["fetch", "--force", "--prune", "origin", targetBranch], "拉取插件远端失败"); + await this.runGitOrThrow(pluginPath, ["reset", "--hard", `origin/${targetBranch}`], "强制更新插件失败"); + await this.runGitOrThrow(pluginPath, ["submodule", "update", "--init", "--recursive"], "更新插件子模块失败", true); + } + + private async rollbackRepository(pluginPath: string, commit: string): Promise { + await runGit(this.gitPath, ["reset", "--hard", commit], pluginPath); + await runGit(this.gitPath, ["submodule", "update", "--init", "--recursive"], pluginPath); + } + + private async runGitOrThrow(pluginPath: string, args: string[], message: string, optional = false): Promise { + const result = await runGit(this.gitPath, args, pluginPath); + if (result.exitCode !== 0 && !optional) { + throw new Error(result.output || message); + } + } + + private async validateInstalledManifest(pluginPath: string, pluginId: string, removeOnFailure = true): Promise { + const manifest = await this.readManifest(pluginPath); + if (!manifest) { + if (removeOnFailure) { + await rm(pluginPath, { recursive: true, force: true }).catch(() => undefined); + } + throw new Error("鏃犳晥鐨勬彃浠讹細缂哄皯 _manifest.json"); + } + + for (const field of ["name", "version", "author"]) { + if (!(field in manifest)) { + if (removeOnFailure) { + await rm(pluginPath, { recursive: true, force: true }).catch(() => undefined); + } + throw new Error(`鏃犳晥鐨?_manifest.json锛氱己灏戝繀闇€瀛楁 ${field}`); + } + } + + return { ...manifest, id: manifest.id?.trim() || pluginId }; + } + + private async readManifest(pluginPath: string): Promise { + const manifestPath = resolve(pluginPath, "_manifest.json"); + if (!isPathInside(pluginPath, manifestPath)) { + return null; + } + + try { + return JSON.parse(await readFile(manifestPath, "utf8")) as MaiBotPluginManifest; + } catch { + return null; + } + } + + private async listRuntimeInstalled(serviceUrl?: string): Promise { + const apiUrl = maibotApiUrl(serviceUrl, "/api/webui/plugins/installed"); + if (!apiUrl) { + return null; + } + + try { + const response = await fetchWithTimeout(apiUrl, MAIBOT_API_TIMEOUT_MS, { + headers: await this.maibotRuntimeAuthHeaders(serviceUrl), + }); + if (!response.ok) { + return null; + } + + const data = (await response.json()) as unknown; + if (!isUnknownRecord(data) || data.success !== true || !Array.isArray(data.plugins)) { + return null; + } + + const plugins = data.plugins + .map(normalizeInstalledPlugin) + .filter((plugin): plugin is MaiBotInstalledPlugin => plugin !== null); + return plugins.sort((left, right) => pluginName(left).localeCompare(pluginName(right), "zh-CN")); + } catch { + return null; + } + } + + private async getRuntimeConfig( + pluginId: string, + pluginPath: string, + configPath: string, + serviceUrl?: string, + ): Promise { + const schemaUrl = maibotApiUrl(serviceUrl, `/api/webui/plugins/config/${encodeURIComponent(pluginId)}/schema`); + const configUrl = maibotApiUrl(serviceUrl, `/api/webui/plugins/config/${encodeURIComponent(pluginId)}`); + if (!schemaUrl || !configUrl) { + return null; + } + + try { + const headers = await this.maibotRuntimeAuthHeaders(serviceUrl); + const [schemaResponse, configResponse] = await Promise.all([ + fetchWithTimeout(schemaUrl, MAIBOT_API_TIMEOUT_MS, { headers }), + fetchWithTimeout(configUrl, MAIBOT_API_TIMEOUT_MS, { headers }), + ]); + if (!schemaResponse.ok || !configResponse.ok) { + return null; + } + + const [schemaPayload, configPayload] = await Promise.all([ + schemaResponse.json() as Promise, + configResponse.json() as Promise, + ]); + const dashboardSchema = readDashboardConfigSchema(schemaPayload); + const config = readDashboardConfig(configPayload); + if (!dashboardSchema || !config) { + return null; + } + + const raw = await this.getRuntimeConfigRaw(pluginId, serviceUrl); + return { + pluginId, + pluginPath, + configPath, + exists: true, + config, + schema: buildDashboardPluginConfigSchema(dashboardSchema, config), + raw: raw ?? stringifyToml(normalizePluginConfigRootForToml(config)), + }; + } catch { + return null; + } + } + + private async getRuntimeConfigRaw(pluginId: string, serviceUrl?: string): Promise { + const rawUrl = maibotApiUrl(serviceUrl, `/api/webui/plugins/config/${encodeURIComponent(pluginId)}/raw`); + if (!rawUrl) { + return null; + } + try { + const response = await fetchWithTimeout(rawUrl, MAIBOT_API_TIMEOUT_MS, { + headers: await this.maibotRuntimeAuthHeaders(serviceUrl), + }); + if (!response.ok) { + return null; + } + const payload = (await response.json()) as unknown; + return readDashboardRawConfig(payload); + } catch { + return null; + } + } + + private async saveRuntimeConfig( + pluginId: string, + config: Record, + pluginPath: string, + configPath: string, + serviceUrl?: string, + ): Promise { + const configUrl = maibotApiUrl(serviceUrl, `/api/webui/plugins/config/${encodeURIComponent(pluginId)}`); + if (!configUrl) { + return null; + } + + try { + const response = await fetchWithTimeout(configUrl, MAIBOT_API_TIMEOUT_MS, { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(await this.maibotRuntimeAuthHeaders(serviceUrl)), + }, + body: JSON.stringify({ config }), + }); + if (!response.ok) { + return null; + } + const payload = (await response.json()) as unknown; + if (!isUnknownRecord(payload) || payload.success !== true) { + return null; + } + const state = await this.getRuntimeConfig(pluginId, pluginPath, configPath, serviceUrl); + if (!state) { + return null; + } + return { + pluginId, + configPath, + config: state.config, + schema: state.schema, + raw: state.raw, + savedAt: Date.now(), + }; + } catch { + return null; + } + } + + private async maibotRuntimeAuthHeaders(serviceUrl?: string): Promise { + const token = tokenFromServiceUrl(serviceUrl) ?? (await this.readMaiBotWebUiToken()); + return token ? { Cookie: `maibot_session=${encodeURIComponent(token)}` } : {}; + } + + private async readMaiBotWebUiToken(): Promise { + const configPath = resolve(this.maibotRoot, "data", "webui.json"); + if (!isPathInside(this.maibotRoot, configPath)) { + return null; + } + + try { + const raw = JSON.parse(await readFile(configPath, "utf8")) as { access_token?: unknown }; + return typeof raw.access_token === "string" && raw.access_token.length > 0 ? raw.access_token : null; + } catch { + return null; + } + } + + private async removePluginPath(pluginPath: string): Promise { + const safePath = this.safePluginPath(basename(pluginPath), true); + await rm(safePath, { recursive: true, force: true }); + } + + private safePluginPath(folderName: string, mustExist: boolean): string { + if (!folderName || folderName.includes("..") || /[\\/\0\r\n\t]/u.test(folderName)) { + throw new Error("鎻掍欢 ID 鍖呭惈闈炴硶瀛楃"); + } + + const targetPath = resolve(this.pluginsRoot, folderName); + if (!isPathInside(this.pluginsRoot, targetPath)) { + throw new Error("鎻掍欢璺緞瓒呭嚭鍏佽鑼冨洿"); + } + if (mustExist && targetPath === this.pluginsRoot) { + throw new Error("拒绝操作插件根目录"); + } + return targetPath; + } +} + +function normalizeMarketPlugin(raw: unknown): MaiBotMarketPlugin | null { + if (!raw || typeof raw !== "object" || !("manifest" in raw)) { + return null; + } + + const item = raw as { + id?: string; + manifest?: MaiBotPluginManifest; + source?: string; + downloads?: unknown; + rating?: unknown; + likes?: unknown; + }; + const manifest = item.manifest; + const id = manifest?.id?.trim() || item.id?.trim(); + if (!manifest || !id || !manifest.name || !manifest.version) { + return null; + } + + return { + id, + manifest: { ...manifest, id }, + source: item.source, + downloads: normalizeStatsNumber(item.downloads), + rating: normalizeStatsNumber(item.rating), + likes: normalizeStatsNumber(item.likes), + }; +} + +function normalizeInstalledPlugin(raw: unknown): MaiBotInstalledPlugin | null { + if (!isUnknownRecord(raw) || !isUnknownRecord(raw.manifest)) { + return null; + } + + const manifest = raw.manifest as MaiBotPluginManifest; + const id = String(raw.id ?? manifest.id ?? "").trim(); + if (!id || !manifest.name || !manifest.version) { + return null; + } + + return { + id, + manifest: { ...manifest, id: manifest.id?.trim() || id }, + path: typeof raw.path === "string" ? raw.path : "", + enabled: typeof raw.enabled === "boolean" ? raw.enabled : typeof raw.disabled === "boolean" ? !raw.disabled : true, + loaded: typeof raw.loaded === "boolean" ? raw.loaded : undefined, + load_status: typeof raw.load_status === "string" ? raw.load_status : undefined, + }; +} + +async function fetchMarketPluginsUncached(marketUrl: string): Promise { + const response = await fetchWithTimeout(marketUrl); + if (!response.ok) { + throw new Error(`Plugin market list failed: HTTP ${response.status}`); + } + + const rawList = (await response.json()) as unknown; + const sourceList = Array.isArray(rawList) ? rawList : []; + return sourceList + .map(normalizeMarketPlugin) + .filter((plugin): plugin is MaiBotMarketPlugin => plugin !== null); +} + +async function fetchPluginStatsSummary(): Promise> { + const response = await fetchWithTimeout(PLUGIN_STATS_URL); + if (!response.ok) { + return {}; + } + + const data = (await response.json()) as unknown; + if (!isUnknownRecord(data) || data.success !== true || !isUnknownRecord(data.stats)) { + return {}; + } + + return Object.fromEntries( + Object.entries(data.stats) + .map(([pluginId, rawStats]) => normalizePluginStats(pluginId, rawStats)) + .filter((entry): entry is [string, MaiBotPluginStats] => entry !== null), + ); +} + +function isMarketPluginList(value: unknown): value is MaiBotMarketPlugin[] { + return Array.isArray(value) && value.every((item) => normalizeMarketPlugin(item) !== null); +} + +function isPluginStatsMap(value: unknown): value is Record { + if (!isUnknownRecord(value)) { + return false; + } + return Object.entries(value).every(([pluginId, stats]) => normalizePluginStats(pluginId, stats) !== null); +} + +function normalizePluginStats(pluginId: string, rawStats: unknown): [string, MaiBotPluginStats] | null { + if (!isUnknownRecord(rawStats)) { + return null; + } + + const normalizedId = String(rawStats.plugin_id ?? pluginId); + return [ + pluginId, + { + plugin_id: normalizedId, + likes: normalizeStatsNumber(rawStats.likes) ?? 0, + dislikes: normalizeStatsNumber(rawStats.dislikes) ?? 0, + downloads: normalizeStatsNumber(rawStats.downloads) ?? 0, + rating: normalizeStatsNumber(rawStats.rating) ?? 0, + rating_count: normalizeStatsNumber(rawStats.rating_count) ?? 0, + recent_ratings: normalizePluginRatings(rawStats.recent_ratings), + }, + ]; +} + +function normalizePluginStatsDetail(pluginId: string, rawData: unknown): MaiBotPluginStats | null { + if (!isUnknownRecord(rawData)) { + return null; + } + const rawStats = isUnknownRecord(rawData.stats) ? rawData.stats : rawData; + const normalized = normalizePluginStats(pluginId, rawStats); + return normalized?.[1] ?? null; +} + +function normalizePluginRatings(rawRatings: unknown): MaiBotPluginStats["recent_ratings"] { + if (!Array.isArray(rawRatings)) { + return undefined; + } + return rawRatings + .filter(isUnknownRecord) + .map((rating) => ({ + user_id: String(rating.user_id ?? "鍖垮悕鐢ㄦ埛"), + rating: normalizeStatsNumber(rating.rating) ?? 0, + comment: typeof rating.comment === "string" ? rating.comment : undefined, + created_at: String(rating.created_at ?? ""), + })) + .filter((rating) => rating.rating > 0 || rating.comment); +} + +function githubRawReadmeUrl(repositoryUrl: string): ((branch: string) => string) | undefined { + const match = repositoryUrl.match(/github\.com[/:]([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:[/?#]|$)/iu); + if (!match) { + return undefined; + } + const [, owner, repo] = match; + return (branch: string) => `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/README.md`; +} + +function rewriteGithubUrl(url: string, sourceMaibotUrl: string): string { + const sourcePrefix = githubSourcePrefix(sourceMaibotUrl); + if (!sourcePrefix) { + return url; + } + + const normalized = normalizeGithubUrl(url); + return normalized ? `${sourcePrefix}${normalized}` : url; +} + +function githubSourcePrefix(sourceMaibotUrl: string): string | undefined { + const normalizedSource = sourceMaibotUrl.trim(); + const officialSourceIndex = normalizedSource.toLowerCase().indexOf(OFFICIAL_MAIBOT_REMOTE_URL.toLowerCase()); + if (officialSourceIndex > 0) { + return normalizedSource.slice(0, officialSourceIndex); + } + return undefined; +} + +function normalizeGithubUrl(url: string): string | undefined { + const trimmed = url.trim(); + if (!trimmed) { + return undefined; + } + + const rawMatch = trimmed.match(/raw\.githubusercontent\.com\/([^/\s]+)\/([^/\s#?]+)\/(.+)$/iu); + if (rawMatch) { + const [, owner, repo, rest] = rawMatch; + return `${OFFICIAL_RAW_GITHUB_BASE_URL}${owner}/${repo.replace(/\.git$/iu, "")}/${rest}`; + } + + const repoMatch = trimmed.match(/github\.com[/:]([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:[/?#]|$)/iu); + if (!repoMatch) { + return undefined; + } + + const [, owner, repo] = repoMatch; + return `${OFFICIAL_GITHUB_BASE_URL}${owner}/${repo.replace(/\.git$/iu, "")}.git`; +} + +function resolvePluginStats( + plugin: { id: string; manifest: MaiBotPluginManifest }, + stats: Record, +): MaiBotPluginStats | undefined { + const statsIds = [plugin.manifest.id, plugin.id].filter((id): id is string => Boolean(id)); + return statsIds.map((id) => stats[id]).find(Boolean); +} + +function normalizeStatsNumber(value: unknown): number | undefined { + const numberValue = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined; + return Number.isFinite(numberValue) ? numberValue : undefined; +} + +function inferPluginId(folderName: string, manifest: MaiBotPluginManifest): string { + if (manifest.id?.trim()) { + return manifest.id.trim(); + } + + const author = typeof manifest.author === "string" ? manifest.author : manifest.author?.name; + const repository = manifest.repository_url?.trim() || manifest.urls?.repository?.trim(); + const repoName = repository ? basename(repository.replace(/\.git$/iu, "")) : undefined; + if (author && repoName) { + return `${author}.${repoName}`; + } + if (author) { + return `${author}.${folderName}`; + } + return folderName.includes("_") && !folderName.includes(".") ? folderName.replace("_", ".") : folderName; +} + +function validatePluginId(pluginId: string): string { + const normalized = pluginId.trim(); + if (!normalized || normalized.startsWith(".") || normalized.endsWith(".")) { + throw new Error("鎻掍欢 ID 涓嶈兘涓虹┖锛屼笖涓嶈兘浠ョ偣寮€澶存垨缁撳熬"); + } + if ([".", ".."].includes(normalized) || /[\\/\0\r\n\t]/u.test(normalized) || normalized.includes("..")) { + throw new Error("鎻掍欢 ID 鍖呭惈闈炴硶瀛楃"); + } + return normalized; +} + +function pluginName(plugin: { id: string; manifest: MaiBotPluginManifest }): string { + return plugin.manifest.name?.trim() || plugin.id; +} + +function pluginVersion(manifest: MaiBotPluginManifest): string { + return manifest.version?.trim() || "unknown"; +} + +function parsePluginConfig(raw: string, configPath: string): Record { + try { + return normalizePluginConfigRoot(parseToml(raw)); + } catch (error) { + throw new Error(`TOML 閰嶇疆瑙f瀽澶辫触: ${configPath}: ${error instanceof Error ? error.message : String(error)}`); + } +} + +function readPluginEnabled(config: Record): boolean { + const pluginSection = config.plugin; + if (!isConfigRecord(pluginSection)) { + return true; + } + const enabled = pluginSection.enabled; + if (typeof enabled === "string") { + const normalized = enabled.trim().toLowerCase(); + if (["0", "false", "no", "off"].includes(normalized)) return false; + if (["1", "true", "yes", "on"].includes(normalized)) return true; + } + return typeof enabled === "boolean" ? enabled : true; +} + +interface DashboardConfigFieldSchema { + name?: unknown; + type?: unknown; + default?: unknown; + description?: unknown; + label?: unknown; + placeholder?: unknown; + hint?: unknown; + icon?: unknown; + ui_type?: unknown; + input_type?: unknown; + choices?: unknown; + min?: unknown; + max?: unknown; + step?: unknown; + rows?: unknown; + required?: unknown; + hidden?: unknown; + disabled?: unknown; + order?: unknown; + item_type?: unknown; + min_items?: unknown; + max_items?: unknown; +} + +interface DashboardConfigSectionSchema { + name?: unknown; + title?: unknown; + description?: unknown; + icon?: unknown; + collapsed?: unknown; + order?: unknown; + fields?: unknown; +} + +interface DashboardConfigSchema { + plugin_id?: unknown; + plugin_info?: unknown; + sections?: unknown; + layout?: unknown; +} + +function readDashboardConfigSchema(payload: unknown): DashboardConfigSchema | null { + if (!isUnknownRecord(payload)) { + return null; + } + const schema = payload.schema; + return isUnknownRecord(schema) && isUnknownRecord(schema.sections) ? schema : null; +} + +function readDashboardConfig(payload: unknown): Record | null { + if (!isUnknownRecord(payload)) { + return null; + } + return isUnknownRecord(payload.config) ? normalizePluginConfigRoot(payload.config) : null; +} + +function readDashboardRawConfig(payload: unknown): string | null { + if (!isUnknownRecord(payload)) { + return null; + } + return typeof payload.config === "string" ? payload.config : null; +} + +function normalizePluginConfigRoot(value: unknown): Record { + if (!isUnknownRecord(value)) { + return {}; + } + + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, normalizePluginConfigValue(item)]), + ); +} + +function normalizePluginConfigValue(value: unknown): MaiBotPluginConfigValue { + if (value === null || value === undefined) { + return null; + } + if (typeof value === "string" || typeof value === "boolean") { + return value; + } + if (typeof value === "number") { + return Number.isFinite(value) ? value : 0; + } + if (value instanceof Date) { + return value.toISOString(); + } + if (Array.isArray(value)) { + return value.map(normalizePluginConfigValue); + } + if (isUnknownRecord(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, normalizePluginConfigValue(item)]), + ); + } + return String(value); +} + +function normalizePluginConfigRootForToml( + value: Record, +): Record { + if (!isUnknownRecord(value)) { + throw new Error("插件配置必须是对象"); + } + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, normalizePluginConfigValueForToml(item, key)]), + ); +} + +function normalizePluginConfigValueForToml(value: MaiBotPluginConfigValue, path: string): MaiBotPluginConfigValue { + if (value === null) { + throw new Error(`TOML 涓嶆敮鎸?null: ${path}`); + } + if (typeof value === "string" || typeof value === "boolean") { + return value; + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new Error(`鏁板瓧閰嶇疆鏃犳晥: ${path}`); + } + return value; + } + if (Array.isArray(value)) { + return value.map((item, index) => normalizePluginConfigValueForToml(item, `${path}.${index}`)); + } + if (isConfigRecord(value)) { + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [ + key, + normalizePluginConfigValueForToml(item, `${path}.${key}`), + ]), + ); + } + throw new Error(`鎻掍欢閰嶇疆鍊间笉鍙楁敮鎸? ${path}`); +} + +function buildPluginConfigSchema( + config: Record, + source: MaiBotPluginConfigSchema["source"] = "local", +): MaiBotPluginConfigSchema { + const generalFields = Object.entries(config) + .filter(([, value]) => !isConfigRecord(value) || Array.isArray(value)) + .map(([key, value]) => buildPluginConfigField([key], key, value)); + + const sections = Object.entries(config) + .filter(([, value]) => isConfigRecord(value) && !Array.isArray(value)) + .map(([sectionName, sectionValue]) => ({ + name: sectionName, + title: labelFromKey(sectionName), + fields: Object.entries(sectionValue as Record).map(([fieldName, fieldValue]) => + buildPluginConfigField([sectionName, fieldName], fieldName, fieldValue), + ), + })); + + if (generalFields.length > 0) { + sections.unshift({ + name: "general", + title: "甯歌", + fields: generalFields, + }); + } + + return { sections, source }; +} + +function buildPluginConfigField(path: string[], name: string, value: MaiBotPluginConfigValue): MaiBotPluginConfigSchema["sections"][number]["fields"][number] { + return { + name, + label: labelFromKey(name), + path, + type: pluginConfigValueType(value), + value, + }; +} + +function buildDashboardPluginConfigSchema( + dashboardSchema: DashboardConfigSchema, + config: Record, +): MaiBotPluginConfigSchema { + const sectionsRecord = isUnknownRecord(dashboardSchema.sections) ? dashboardSchema.sections : {}; + const sections = Object.entries(sectionsRecord) + .map(([sectionKey, rawSection]) => normalizeDashboardSection(sectionKey, rawSection, config)) + .filter((section): section is MaiBotPluginConfigSchema["sections"][number] => section !== null) + .sort((left, right) => (left.order ?? 0) - (right.order ?? 0)); + + const pluginInfo = isUnknownRecord(dashboardSchema.plugin_info) + ? { + name: localizedTextOrUndefined(dashboardSchema.plugin_info.name), + version: stringOrUndefined(dashboardSchema.plugin_info.version), + description: localizedTextOrUndefined(dashboardSchema.plugin_info.description), + author: stringOrUndefined(dashboardSchema.plugin_info.author), + } + : undefined; + const layout = normalizeDashboardLayout(dashboardSchema.layout); + + return { pluginInfo, sections, layout, source: "runtime" }; +} + +function normalizeDashboardSection( + sectionKey: string, + rawSection: unknown, + config: Record, +): MaiBotPluginConfigSchema["sections"][number] | null { + if (!isUnknownRecord(rawSection)) { + return null; + } + const section = rawSection as DashboardConfigSectionSchema; + const fieldsRecord = isUnknownRecord(section.fields) ? section.fields : {}; + const sectionName = stringOrUndefined(section.name) ?? sectionKey; + const fields = Object.entries(fieldsRecord) + .map(([fieldKey, rawField]) => normalizeDashboardField(sectionName, fieldKey, rawField, config)) + .filter((field): field is MaiBotPluginConfigSchema["sections"][number]["fields"][number] => field !== null) + .filter((field) => field.hidden !== true) + .sort((left, right) => (left.order ?? 0) - (right.order ?? 0)); + + return { + name: sectionName, + title: localizedTextOrUndefined(section.title) ?? labelFromKey(sectionName), + description: localizedTextOrUndefined(section.description), + icon: stringOrUndefined(section.icon), + collapsed: typeof section.collapsed === "boolean" ? section.collapsed : undefined, + order: numberOrUndefined(section.order), + fields, + }; +} + +function normalizeDashboardField( + sectionName: string, + fieldKey: string, + rawField: unknown, + config: Record, +): MaiBotPluginConfigSchema["sections"][number]["fields"][number] | null { + if (!isUnknownRecord(rawField)) { + return null; + } + const field = rawField as DashboardConfigFieldSchema; + const name = stringOrUndefined(field.name) ?? fieldKey; + const path = [sectionName, name]; + const configValue = getConfigValueAtPath(config, path); + const defaultValue = field.default === undefined ? undefined : normalizePluginConfigValue(field.default); + const value = configValue ?? defaultValue ?? defaultValueForDashboardType(field.type); + return { + name, + label: localizedTextOrUndefined(field.label) ?? labelFromKey(name), + path, + type: pluginConfigValueType(value), + value, + description: localizedTextOrUndefined(field.description), + hint: localizedTextOrUndefined(field.hint), + placeholder: localizedTextOrUndefined(field.placeholder), + uiType: stringOrUndefined(field.ui_type), + inputType: stringOrUndefined(field.input_type), + choices: normalizeDashboardChoices(field.choices), + min: numberOrUndefined(field.min), + max: numberOrUndefined(field.max), + step: numberOrUndefined(field.step), + rows: numberOrUndefined(field.rows), + required: typeof field.required === "boolean" ? field.required : undefined, + hidden: typeof field.hidden === "boolean" ? field.hidden : undefined, + disabled: typeof field.disabled === "boolean" ? field.disabled : undefined, + order: numberOrUndefined(field.order), + icon: stringOrUndefined(field.icon), + default: defaultValue, + itemType: stringOrUndefined(field.item_type), + minItems: numberOrUndefined(field.min_items), + maxItems: numberOrUndefined(field.max_items), + }; +} + +function normalizeDashboardLayout(rawLayout: unknown): MaiBotPluginConfigSchema["layout"] { + if (!isUnknownRecord(rawLayout)) { + return undefined; + } + const rawTabs = Array.isArray(rawLayout.tabs) ? rawLayout.tabs : []; + const tabs = rawTabs + .filter(isUnknownRecord) + .map((tab) => ({ + id: stringOrUndefined(tab.id) ?? "", + title: localizedTextOrUndefined(tab.title) ?? "", + sections: Array.isArray(tab.sections) ? tab.sections.map(String) : [], + icon: stringOrUndefined(tab.icon), + order: numberOrUndefined(tab.order), + badge: stringOrUndefined(tab.badge), + })) + .filter((tab) => tab.id && tab.title) + .sort((left, right) => (left.order ?? 0) - (right.order ?? 0)); + const type = rawLayout.type === "tabs" || rawLayout.type === "pages" ? rawLayout.type : "auto"; + return { type, tabs }; +} + +function normalizeDashboardChoices( + choices: unknown, +): MaiBotPluginConfigSchema["sections"][number]["fields"][number]["choices"] { + if (!Array.isArray(choices)) { + return undefined; + } + return choices.map((choice) => { + if (isUnknownRecord(choice) && "value" in choice) { + return { + label: localizedTextOrUndefined(choice.label), + value: normalizePluginConfigValue(choice.value), + }; + } + return normalizePluginConfigValue(choice); + }); +} + +function getConfigValueAtPath(config: Record, path: string[]): MaiBotPluginConfigValue | undefined { + let cursor: MaiBotPluginConfigValue | Record = config; + for (const segment of path) { + if (!isConfigRecord(cursor) || !(segment in cursor)) { + return undefined; + } + cursor = cursor[segment]; + } + return cursor as MaiBotPluginConfigValue; +} + +function defaultValueForDashboardType(type: unknown): MaiBotPluginConfigValue { + switch (type) { + case "boolean": + return false; + case "number": + case "integer": + return 0; + case "array": + case "list": + return []; + case "object": + return {}; + default: + return ""; + } +} + +function localizedTextOrUndefined(value: unknown): MaiBotPluginConfigLocalizedText | undefined { + if (typeof value === "string") { + return value; + } + if (!isUnknownRecord(value)) { + return undefined; + } + const entries = Object.entries(value) + .filter((entry): entry is [string, string] => typeof entry[1] === "string"); + return entries.length > 0 ? Object.fromEntries(entries) : undefined; +} + +function stringOrUndefined(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value : undefined; +} + +function numberOrUndefined(value: unknown): number | undefined { + const numberValue = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined; + return Number.isFinite(numberValue) ? numberValue : undefined; +} + +function pluginConfigValueType(value: MaiBotPluginConfigValue): MaiBotPluginConfigSchema["sections"][number]["fields"][number]["type"] { + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + if (isConfigRecord(value)) return "object"; + if (typeof value === "boolean") return "boolean"; + if (typeof value === "number") return "number"; + return "string"; +} + +function labelFromKey(value: string): string { + return value + .replace(/[_-]+/gu, " ") + .replace(/\s+/gu, " ") + .trim() + || value; +} + +function isUnknownRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isConfigRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isNewerVersion(candidate: string, current: string): boolean { + const candidateParts = normalizeVersion(candidate); + const currentParts = normalizeVersion(current); + const width = Math.max(candidateParts.length, currentParts.length); + for (let index = 0; index < width; index++) { + const diff = (candidateParts[index] ?? 0) - (currentParts[index] ?? 0); + if (diff !== 0) { + return diff > 0; + } + } + return false; +} + +function normalizeVersion(version: string): number[] { + return version + .trim() + .toLowerCase() + .replace(/^v/u, "") + .split(/[+-]/u, 1)[0] + .split(/[._-]/u) + .map((part) => { + const value = part.match(/^\d+/u)?.[0]; + return value ? Number(value) : 0; + }); +} + +function isPathInside(root: string, target: string): boolean { + const resolvedRoot = resolve(root); + const resolvedTarget = resolve(target); + const pathDiff = relative(resolvedRoot, resolvedTarget); + return !pathDiff || (pathDiff !== ".." && !pathDiff.startsWith(`..${sep}`) && !isAbsolute(pathDiff)); +} + +async function pathExists(path: string): Promise { + try { + await stat(path); + return true; + } catch { + return false; + } +} + +async function isDirectory(path: string): Promise { + try { + return (await stat(path)).isDirectory(); + } catch { + return false; + } +} + +function maibotApiUrl(serviceUrl: string | undefined, path: string): string | null { + try { + const base = new URL(serviceUrl ?? "http://127.0.0.1:8001"); + return new URL(path, base.origin).toString(); + } catch { + return null; + } +} + +function tokenFromServiceUrl(serviceUrl: string | undefined): string | null { + if (!serviceUrl) { + return null; + } + try { + return new URL(serviceUrl).searchParams.get("token"); + } catch { + return null; + } +} + +async function fetchWithTimeout(url: string, timeoutMs = MARKET_TIMEOUT_MS, init?: RequestInit): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timeout); + } +} + +function runGit(gitPath: string, args: string[], cwd: string): Promise { + return new Promise((resolveResult) => { + execFile( + gitPath, + args, + { + cwd, + timeout: 120_000, + windowsHide: true, + maxBuffer: 1024 * 1024 * 8, + env: { + ...process.env, + GIT_TERMINAL_PROMPT: "0", + }, + }, + (error, stdout, stderr) => { + const output = `${stdout}${stderr}`.trim(); + resolveResult({ + exitCode: typeof error?.code === "number" ? error.code : error ? 1 : 0, + output, + }); + }, + ); + }); +} diff --git a/src/main/services/module-updater.ts b/src/main/services/module-updater.ts new file mode 100644 index 0000000..b23057e --- /dev/null +++ b/src/main/services/module-updater.ts @@ -0,0 +1,510 @@ +import { execFile } from "node:child_process"; +import { existsSync } from "node:fs"; +import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import type { + ModuleSourceConfig, + ModuleSourceOption, + ModuleSourcePreset, + ModuleSourceUpdate, + ModuleTagOption, + ModuleUpdateResult, + RuntimePaths, +} from "../../shared/contracts"; +import { InitManager } from "./init-manager"; + +const UPDATE_TIMEOUT_MS = 15 * 60 * 1000; +/** 单次 git fetch origin 的最长等待时间。失败/超时后会恢复到更新前状态。 */ +const FETCH_ORIGIN_TIMEOUT_MS = 15 * 60 * 1000; +const OFFICIAL_MAIBOT_REMOTE_URL = "https://github.com/Mai-with-u/MaiBot.git"; +const OFFICIAL_NAPCAT_ADAPTER_REMOTE_URL = "https://github.com/Mai-with-u/MaiBot-Napcat-Adapter.git"; +const GHPROXY_MAIBOT_REMOTE_URL = "https://gh.llkk.cc/https://github.com/Mai-with-u/MaiBot.git"; +const GHPROXY_NAPCAT_ADAPTER_REMOTE_URL = + "https://gh.llkk.cc/https://github.com/Mai-with-u/MaiBot-Napcat-Adapter.git"; +const SOURCE_CONFIG_FILE = "module-sources.json"; +const SOURCE_OPTIONS: ModuleSourceOption[] = [ + { + preset: "ghproxy", + label: "GitHub 镜像代理", + maibotUrl: GHPROXY_MAIBOT_REMOTE_URL, + napcatAdapterUrl: GHPROXY_NAPCAT_ADAPTER_REMOTE_URL, + }, + { + preset: "official", + label: "官方 GitHub", + maibotUrl: OFFICIAL_MAIBOT_REMOTE_URL, + napcatAdapterUrl: OFFICIAL_NAPCAT_ADAPTER_REMOTE_URL, + }, +]; + +interface GitRunResult { + output: string[]; +} + +interface RepoUpdateSpec { + moduleId: ModuleUpdateResult["moduleId"]; + moduleName: string; + cwd: string; + bundledDir: string; + remoteUrl: string; + defaultBranch: string; + /** 是否在更新失败时把错误抛到外层(false 时仅返回 result,原地保留错误信息)。 */ + throwOnFailure: boolean; + /** 是否执行 git submodule 更新(仅主仓需要)。 */ + runSubmodule: boolean; + targetTag?: string; +} + +function isPrereleaseTag(tag: string): boolean { + return /(?:^|[._+-])(?:a|alpha|b|beta|rc|pre|preview|dev)\d*/iu.test(tag); +} + +function splitOutput(output: string): string[] { + return output + .replace(/\r\n/gu, "\n") + .replace(/\r/gu, "\n") + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0); +} + +function toDetail(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export class ModuleUpdater { + private readonly sourceConfigPath: string; + + constructor( + private readonly paths: RuntimePaths, + private readonly initManager: InitManager, + ) { + this.sourceConfigPath = join(paths.userDataRoot, SOURCE_CONFIG_FILE); + } + + async getSourceConfig(): Promise { + return this.resolveSourceConfig(await this.readStoredSourceConfig()); + } + + async saveSourceConfig(update: ModuleSourceUpdate): Promise { + const config = this.resolveSourceConfig(update); + await mkdir(dirname(this.sourceConfigPath), { recursive: true }); + await writeFile( + this.sourceConfigPath, + `${JSON.stringify( + { + version: 1, + preset: config.preset, + maibotUrl: config.maibotUrl, + napcatAdapterUrl: config.napcatAdapterUrl, + }, + null, + 2, + )}\n`, + "utf8", + ); + return config; + } + + async listMaiBotTags(): Promise { + const gitPath = this.initManager.getGitPath(); + const sourceConfig = await this.getSourceConfig(); + const result = await this.runGit( + gitPath, + this.paths.installRoot, + ["ls-remote", "--tags", "--refs", sourceConfig.maibotUrl], + FETCH_ORIGIN_TIMEOUT_MS, + ); + return result.output + .map((line) => line.match(/refs\/tags\/(.+)$/u)?.[1]) + .filter((tag): tag is string => Boolean(tag)) + .sort((left, right) => right.localeCompare(left, "en-US", { numeric: true, sensitivity: "base" })) + .slice(0, 80) + .map((name) => ({ name, isPrerelease: isPrereleaseTag(name) })); + } + + async updateMaiBot(targetTag?: string): Promise { + const gitPath = this.initManager.getGitPath(); + if (!existsSync(gitPath)) { + throw new Error(`未找到可用 Git: ${gitPath}`); + } + + const sourceConfig = await this.getSourceConfig(); + + // 主仓 + const mainResult = await this.updateGitRepository(gitPath, { + moduleId: "maibot", + moduleName: "MaiBot", + cwd: this.paths.maibotRoot, + bundledDir: join(this.paths.bundledModulesRoot, "MaiBot"), + remoteUrl: sourceConfig.maibotUrl, + defaultBranch: "main", + throwOnFailure: true, + runSubmodule: true, + targetTag: targetTag?.trim() || undefined, + }); + return mainResult; + } + + /** + * 直接用一键包内置的 napcat-adapter 快照覆盖可写目录里的对应插件,不走任何网络。 + * 适用场景:用户因 .gitignore 历史问题导致 plugins/napcat-adapter/runtime/ 缺失, + * 报 `[E_PLUGIN_NOT_FOUND] No module named '_maibot_plugin_maibot_team_napcat_adapter.runtime'`, + * 又不想等 git fetch 联网。强制清空再整目录复制 bundled,含 .git。 + */ + async repairNapcatAdapterFromBundled(): Promise { + const moduleId: ModuleUpdateResult["moduleId"] = "napcat-adapter"; + const moduleName = "napcat-adapter"; + const cwd = join(this.paths.maibotRoot, "plugins", "napcat-adapter"); + const bundled = join(this.paths.bundledModulesRoot, "MaiBot", "plugins", "napcat-adapter"); + const gitPath = this.initManager.getGitPath(); + const output: string[] = []; + + if (!existsSync(bundled)) { + throw new Error(`一键包内置的 napcat-adapter 模板缺失: ${bundled}`); + } + let bundledStat: Awaited>; + try { + bundledStat = await stat(bundled); + } catch (err) { + throw new Error(`无法读取一键包内置 napcat-adapter 模板: ${toDetail(err)}`); + } + if (!bundledStat.isDirectory()) { + throw new Error(`一键包内置 napcat-adapter 路径不是目录: ${bundled}`); + } + + output.push(`[${moduleName}] 使用一键包内置快照修复(不联网,强制覆盖整个插件目录)。`); + output.push(`[${moduleName}] 来源: ${bundled}`); + output.push(`[${moduleName}] 目标: ${cwd}`); + + if (existsSync(cwd)) { + output.push(`[${moduleName}] 删除既有目录...`); + await rm(cwd, { recursive: true, force: true }); + } + + output.push(`[${moduleName}] 复制内置快照(含 .git)...`); + await cp(bundled, cwd, { + recursive: true, + force: true, + errorOnExist: false, + }); + + let after: string | undefined; + if (existsSync(join(cwd, ".git"))) { + after = await this.readGitValue(gitPath, cwd, ["rev-parse", "--short", "HEAD"]); + } + + output.push(`[${moduleName}] ✓ 修复完成。`); + + return { + moduleId, + moduleName, + cwd, + gitPath, + changed: true, + after, + output, + updatedAt: Date.now(), + source: "bundled", + warning: + "已使用一键包内置 napcat-adapter 快照覆盖修复。此快照与本一键包发布日同步,可能落后于上游最新代码;建议稍后在网络恢复时点击「更新 MaiBot」拉取最新版本。", + }; + } + + private async updateGitRepository( + gitPath: string, + spec: RepoUpdateSpec, + ): Promise { + const { cwd, bundledDir, remoteUrl, defaultBranch, moduleId, moduleName } = spec; + + if (!existsSync(cwd)) { + // 模块目录不存在:若 bundled 里有,则尝试从 bundled 复制 .git 后再走 reset 流程; + // 这里简单抛错让上层(init-manager 的 ensure 流程)先确保目录存在。 + throw new Error(`模块目录不存在: ${cwd}`); + } + + const output: string[] = []; + const append = (label: string, lines: string[]): void => { + output.push(`$ git ${label}`); + output.push(...lines); + }; + + if (!existsSync(join(cwd, ".git"))) { + output.push( + `[${moduleName}] 未发现 .git,正在接入官方 Git 仓库;不会清理 data/logs/config 等用户数据目录。`, + ); + append(`[${moduleName}] init`, (await this.runGit(gitPath, cwd, ["init"], 30_000)).output); + } + + let remote = await this.readGitValue(gitPath, cwd, ["config", "--get", "remote.origin.url"]); + const originalRemote = remote; + const hadOriginRemote = Boolean(remote); + if (!remote) { + append( + `[${moduleName}] remote add origin ${remoteUrl}`, + (await this.runGit(gitPath, cwd, ["remote", "add", "origin", remoteUrl], 30_000)).output, + ); + remote = remoteUrl; + } else if (remote !== remoteUrl) { + append( + `[${moduleName}] remote set-url origin ${remoteUrl}`, + (await this.runGit(gitPath, cwd, ["remote", "set-url", "origin", remoteUrl], 30_000)).output, + ); + remote = remoteUrl; + } + + const bundledHasGit = bundledDir !== cwd && existsSync(join(bundledDir, ".git")); + if (bundledHasGit) { + const existingBundledUrl = await this.readGitValue(gitPath, cwd, [ + "config", + "--get", + "remote.bundled.url", + ]); + if (!existingBundledUrl) { + append( + `[${moduleName}] remote add bundled ${bundledDir}`, + (await this.runGit(gitPath, cwd, ["remote", "add", "bundled", bundledDir], 30_000)).output, + ); + } else if (existingBundledUrl !== bundledDir) { + append( + `[${moduleName}] remote set-url bundled ${bundledDir}`, + (await this.runGit(gitPath, cwd, ["remote", "set-url", "bundled", bundledDir], 30_000)).output, + ); + } + } + + const before = await this.readGitValue(gitPath, cwd, ["rev-parse", "--short", "HEAD"]); + const branch = await this.readGitValue(gitPath, cwd, ["branch", "--show-current"]); + const statusBefore = await this.readGitValue(gitPath, cwd, ["status", "--short"]); + if (statusBefore) { + output.push( + `[${moduleName}] 检测到工作区存在本地改动;本次强制更新会覆盖代码改动,但不会清理 data/logs/config 等用户数据目录。`, + ); + } + + let warning: string | undefined; + let remoteError: string | undefined; + let upstream: string; + + try { + append( + `[${moduleName}] fetch origin --prune --tags --force --progress (timeout ${Math.round(FETCH_ORIGIN_TIMEOUT_MS / 1000)}s)`, + ( + await this.runGit( + gitPath, + cwd, + ["fetch", "origin", "--prune", "--tags", "--force", "--progress"], + FETCH_ORIGIN_TIMEOUT_MS, + ) + ).output, + ); + upstream = spec.targetTag ? `refs/tags/${spec.targetTag}` : await this.resolveUpstream(gitPath, cwd, branch ?? defaultBranch); + append( + `[${moduleName}] reset --hard ${upstream}`, + (await this.runGit(gitPath, cwd, ["reset", "--hard", upstream])).output, + ); + } catch (originErr) { + remoteError = toDetail(originErr); + output.push(`[${moduleName}] 远端拉取或更新失败: ${remoteError}`); + await this.restoreRepositoryBeforeUpdate(gitPath, cwd, moduleName, before, originalRemote, hadOriginRemote, output); + const failure = spec.targetTag + ? `无法拉取远端 tag ${spec.targetTag},已恢复到更新前状态: ${remoteError}` + : `远端更新失败,已恢复到更新前状态: ${remoteError}`; + if (spec.throwOnFailure) { + throw new Error(failure); + } + return { + moduleId, + moduleName, + cwd, + gitPath, + remote: originalRemote ?? remote, + branch, + before, + changed: false, + output: [...output, failure], + updatedAt: Date.now(), + source: "remote", + warning: failure, + remoteError, + }; + } + + if (spec.runSubmodule) { + try { + append( + `[${moduleName}] submodule update --init --recursive --force`, + (await this.runGit(gitPath, cwd, ["submodule", "update", "--init", "--recursive", "--force"])).output, + ); + } catch (subErr) { + if (spec.throwOnFailure) { + const remoteError = toDetail(subErr); + output.push(`[${moduleName}] 子模块更新失败: ${remoteError}`); + await this.restoreRepositoryBeforeUpdate(gitPath, cwd, moduleName, before, originalRemote, hadOriginRemote, output); + throw new Error(`子模块更新失败,已恢复到更新前状态: ${remoteError}`); + } else { + output.push(`[${moduleName}] 子模块更新失败(已忽略): ${toDetail(subErr)}`); + } + } + } + + const after = await this.readGitValue(gitPath, cwd, ["rev-parse", "--short", "HEAD"]); + + return { + moduleId, + moduleName, + cwd, + gitPath, + remote, + branch, + upstream, + before, + after, + changed: before ? Boolean(after && before !== after) : Boolean(after), + output, + updatedAt: Date.now(), + source: "remote", + warning, + remoteError, + }; + } + + private async resolveUpstream(gitPath: string, cwd: string, branch?: string): Promise { + const configuredUpstream = await this.readGitValue(gitPath, cwd, [ + "rev-parse", + "--abbrev-ref", + "--symbolic-full-name", + "@{u}", + ]); + if (configuredUpstream) { + return configuredUpstream; + } + + const originHead = await this.readGitValue(gitPath, cwd, [ + "symbolic-ref", + "--quiet", + "--short", + "refs/remotes/origin/HEAD", + ]); + if (originHead) { + return originHead; + } + + if (branch) { + return `origin/${branch}`; + } + + return "origin/main"; + } + + private async restoreRepositoryBeforeUpdate( + gitPath: string, + cwd: string, + moduleName: string, + before: string | undefined, + originalRemote: string | undefined, + hadOriginRemote: boolean, + output: string[], + ): Promise { + try { + if (before) { + output.push(`[${moduleName}] 恢复到更新前提交 ${before} ...`); + output.push(...(await this.runGit(gitPath, cwd, ["reset", "--hard", before], 60_000)).output); + } + } catch (restoreError) { + output.push(`[${moduleName}] 恢复提交失败: ${toDetail(restoreError)}`); + } + + try { + if (hadOriginRemote && originalRemote) { + output.push(`[${moduleName}] 恢复 origin: ${originalRemote}`); + output.push(...(await this.runGit(gitPath, cwd, ["remote", "set-url", "origin", originalRemote], 30_000)).output); + } else { + output.push(`[${moduleName}] 移除本次新增的 origin`); + output.push(...(await this.runGit(gitPath, cwd, ["remote", "remove", "origin"], 30_000)).output); + } + } catch (restoreError) { + output.push(`[${moduleName}] 恢复 origin 失败: ${toDetail(restoreError)}`); + } + } + + private async readStoredSourceConfig(): Promise { + try { + const raw = JSON.parse(await readFile(this.sourceConfigPath, "utf8")) as Partial; + return { + preset: raw.preset ?? "ghproxy", + maibotUrl: raw.maibotUrl, + napcatAdapterUrl: raw.napcatAdapterUrl, + }; + } catch { + return undefined; + } + } + + private resolveSourceConfig(update?: ModuleSourceUpdate): ModuleSourceConfig { + const preset = this.normalizePreset(update?.preset); + const option = SOURCE_OPTIONS.find((item) => item.preset === preset); + const maibotUrl = preset === "custom" ? update?.maibotUrl?.trim() : option?.maibotUrl; + const napcatAdapterUrl = preset === "custom" ? update?.napcatAdapterUrl?.trim() : option?.napcatAdapterUrl; + + if (!maibotUrl || !napcatAdapterUrl) { + throw new Error("自定义模块更新源需要同时填写 MaiBot 与 napcat-adapter 仓库地址。"); + } + + return { + preset, + maibotUrl, + napcatAdapterUrl, + options: SOURCE_OPTIONS, + }; + } + + private normalizePreset(preset: ModuleSourcePreset | undefined): ModuleSourcePreset { + return preset === "official" || preset === "custom" ? preset : "ghproxy"; + } + + private async readGitValue(gitPath: string, cwd: string, args: string[]): Promise { + try { + const result = await this.runGit(gitPath, cwd, args, 15_000); + return result.output.join("\n").trim() || undefined; + } catch { + return undefined; + } + } + + private runGit( + gitPath: string, + cwd: string, + args: string[], + timeoutMs = UPDATE_TIMEOUT_MS, + ): Promise { + return new Promise((resolve, reject) => { + execFile( + gitPath, + args, + { + cwd, + timeout: timeoutMs, + windowsHide: true, + maxBuffer: 8 * 1024 * 1024, + env: { + ...process.env, + GCM_INTERACTIVE: "Never", + GIT_TERMINAL_PROMPT: "0", + LC_ALL: "C.UTF-8", + LANG: "C.UTF-8", + }, + }, + (error, stdout, stderr) => { + const output = splitOutput(`${stdout}${stderr}`); + if (error) { + reject(new Error(output.join("\n") || toDetail(error))); + return; + } + + resolve({ output }); + }, + ); + }); + } +} diff --git a/src/main/services/paths.ts b/src/main/services/paths.ts new file mode 100644 index 0000000..1fd7607 --- /dev/null +++ b/src/main/services/paths.ts @@ -0,0 +1,122 @@ +import { app } from "electron"; +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import type { RuntimePaths, RuntimeResourcePathKey } from "../../shared/contracts"; + +export const RESOURCE_PATHS_FILE = "resource-paths.json"; +export const LEGACY_RESOURCE_LOCATION_FILE = "resource-location.json"; + +type RuntimeResourcePathMap = Record; + +interface StoredResourcePathsFile { + version: 1; + paths?: Partial; +} + +interface LegacyStoredResourceLocationFile { + version: 1; + resourceRoot?: string; +} + +function resolveInstallRoot(): string { + return app.isPackaged ? dirname(process.execPath) : process.cwd(); +} + +function resolvePayloadRoot(installRoot: string): string { + return app.isPackaged ? process.resourcesPath : installRoot; +} + +function createInstallScope(installRoot: string): string { + return createHash("sha256").update(installRoot).digest("hex").slice(0, 12); +} + +function normalizePath(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? resolve(trimmed) : undefined; +} + +function defaultResourcePaths(defaultResourceRoot: string): RuntimeResourcePathMap { + return { + maibot: join(defaultResourceRoot, "modules", "MaiBot"), + napcat: join(defaultResourceRoot, "modules", "napcat"), + pythonOverrides: join(defaultResourceRoot, "python-overrides"), + }; +} + +function readStoredResourcePaths(userDataRoot: string): Partial { + const storePath = join(userDataRoot, RESOURCE_PATHS_FILE); + if (existsSync(storePath)) { + try { + const raw = JSON.parse(readFileSync(storePath, "utf8")) as StoredResourcePathsFile; + return { + maibot: normalizePath(raw.paths?.maibot), + napcat: normalizePath(raw.paths?.napcat), + pythonOverrides: normalizePath(raw.paths?.pythonOverrides), + }; + } catch { + return {}; + } + } + + const legacyStorePath = join(userDataRoot, LEGACY_RESOURCE_LOCATION_FILE); + if (!existsSync(legacyStorePath)) { + return {}; + } + + try { + const raw = JSON.parse(readFileSync(legacyStorePath, "utf8")) as LegacyStoredResourceLocationFile; + const resourceRoot = normalizePath(raw.resourceRoot); + return resourceRoot ? defaultResourcePaths(resourceRoot) : {}; + } catch { + return {}; + } +} + +export function applyRuntimeResourcePaths(paths: RuntimePaths, updates: Partial): void { + if (updates.maibot) { + paths.maibotRoot = updates.maibot; + } + if (updates.napcat) { + paths.napcatRoot = updates.napcat; + } + if (updates.pythonOverrides) { + paths.pythonOverridesRoot = updates.pythonOverrides; + } + paths.resourceRoot = paths.defaultResourceRoot; + paths.modulesRoot = join(paths.defaultResourceRoot, "modules"); +} + +export function configureRuntimePaths(): RuntimePaths { + const installRoot = resolveInstallRoot(); + const payloadRoot = resolvePayloadRoot(installRoot); + const installScope = createInstallScope(installRoot); + const userDataRoot = join(app.getPath("appData"), "MaiBotOneKeyDesktop", installScope); + const defaultResourceRoot = app.isPackaged ? userDataRoot : installRoot; + const defaults = defaultResourcePaths(defaultResourceRoot); + const stored = readStoredResourcePaths(userDataRoot); + const bundledModulesRoot = join(payloadRoot, "modules"); + + app.setPath("userData", userDataRoot); + + const paths: RuntimePaths = { + installRoot, + userDataRoot, + defaultResourceRoot, + resourceRoot: defaultResourceRoot, + modulesRoot: join(defaultResourceRoot, "modules"), + defaultMaibotRoot: defaults.maibot, + maibotRoot: defaults.maibot, + defaultNapcatRoot: defaults.napcat, + napcatRoot: defaults.napcat, + defaultSnowlumaRoot: join(defaultResourceRoot, "modules", "SnowLuma"), + snowlumaRoot: join(defaultResourceRoot, "modules", "SnowLuma"), + bundledModulesRoot, + runtimeRoot: join(payloadRoot, "runtime"), + defaultPythonOverridesRoot: defaults.pythonOverrides, + pythonOverridesRoot: defaults.pythonOverrides, + logsRoot: join(userDataRoot, "logs"), + }; + applyRuntimeResourcePaths(paths, stored); + return paths; +} diff --git a/src/main/services/python-dependency-manager.ts b/src/main/services/python-dependency-manager.ts new file mode 100644 index 0000000..754f560 --- /dev/null +++ b/src/main/services/python-dependency-manager.ts @@ -0,0 +1,988 @@ +import { execFile, spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { delimiter, dirname, join } from "node:path"; +import type { + ManagedPythonPackage, + ManagedPythonPackageName, + PythonOverridesState, + PythonPackageSourceOption, + PythonPackageSourcePreset, + PythonPackageInstallRequest, + PythonPackageInstallResult, + PythonPackageVersion, + PythonPackageVersionList, + RuntimePaths, +} from "../../shared/contracts"; +import { InitManager } from "./init-manager"; + +const TUNA_PYPI_ROOT = "https://pypi.tuna.tsinghua.edu.cn"; +const TUNA_SIMPLE_INDEX = `${TUNA_PYPI_ROOT}/simple`; +const PYPI_SIMPLE_INDEX = "https://pypi.org/simple"; +const ALIYUN_SIMPLE_INDEX = "https://mirrors.aliyun.com/pypi/simple"; +const PYTHON_SOURCE_FILE = "python-dependency-source.json"; +const PIP_INDEXES: Array = [ + { preset: "tuna", label: "清华源", url: TUNA_SIMPLE_INDEX, trustedHost: "pypi.tuna.tsinghua.edu.cn" }, + { preset: "pypi", label: "官方 PyPI", url: PYPI_SIMPLE_INDEX }, + { preset: "aliyun", label: "阿里源", url: ALIYUN_SIMPLE_INDEX, trustedHost: "mirrors.aliyun.com" }, +] as const; +const MANAGED_PACKAGES: ManagedPythonPackage[] = [ + { name: "maibot-dashboard", label: "MaiBot Dashboard" }, + { name: "maim-message", label: "Maim Message" }, +]; +const PYTHON_OVERLAY_TARGET_ENV = "MAIBOT_PYTHON_OVERLAY_TARGET"; +const REQUEST_TIMEOUT_MS = 60_000; +const PIP_TIMEOUT_MS = 10 * 60 * 1000; +const SIMPLE_ACCEPT = "application/vnd.pypi.simple.v1+json, application/json;q=0.9, text/html;q=0.8"; + +interface SimpleProjectFile { + filename?: unknown; + "upload-time"?: unknown; + yanked?: unknown; +} + +interface SimpleProjectJson { + files?: SimpleProjectFile[]; + versions?: unknown; +} + +interface FetchTextResult { + contentType: string; + text: string; +} + +interface StartupDependencyUpgradeResult { + sourceFile: string; + sourceUrl: string; + targetDir: string; + output: string[]; + installedAt: number; +} + +type PythonOutputHandler = (line: string) => void; + +interface UnsatisfiedDependency { + requirement: string; + reason: string; +} + +interface PipInstallAttemptResult { + output: string[]; + sourceUrl: string; +} + +function splitOutput(output: string): string[] { + return output + .replace(/\r\n/gu, "\n") + .replace(/\r/gu, "\n") + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0); +} + +function toDetail(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function assertManagedPackage(packageName: ManagedPythonPackageName): void { + if (!MANAGED_PACKAGES.some((item) => item.name === packageName)) { + throw new Error(`涓嶆敮鎸佹洿鏂版 Python 渚濊禆: ${packageName}`); + } +} + +function isDevVersion(version: string): boolean { + return /(?:^|[._+-])dev\d*/iu.test(version); +} + +function isPrereleaseVersion(version: string): boolean { + return isDevVersion(version) || /(?:^|[._+-])(?:a|alpha|b|beta|rc|pre|preview)\d*/iu.test(version); +} + +function isYanked(value: unknown): boolean { + return value === true || (typeof value === "string" && value.length > 0); +} + +function uploadTime(raw: unknown): { uploadedAt?: string; uploadedAtMs?: number } { + if (typeof raw !== "string" || raw.length === 0) { + return {}; + } + + const timestamp = Date.parse(raw); + if (!Number.isFinite(timestamp)) { + return {}; + } + + return { + uploadedAt: raw, + uploadedAtMs: timestamp, + }; +} + +function sortVersions(versions: PythonPackageVersion[]): PythonPackageVersion[] { + return versions.sort((left, right) => { + const byTime = (right.uploadedAtMs ?? 0) - (left.uploadedAtMs ?? 0); + if (byTime !== 0) { + return byTime; + } + + return right.version.localeCompare(left.version, "en-US", { numeric: true, sensitivity: "base" }); + }); +} + +function versionEntry(version: string, upload?: { uploadedAt?: string; uploadedAtMs?: number }): PythonPackageVersion { + return { + version, + isPrerelease: isPrereleaseVersion(version), + isDev: isDevVersion(version), + ...upload, + }; +} + +function normalizeProjectName(name: string): string { + return name.toLowerCase().replace(/[-_.]+/gu, "-"); +} + +function packageImportName(name: string): string { + return normalizeProjectName(name).replace(/-/gu, "_"); +} + +function packageNameFromRequirement(requirement: string): string | undefined { + return requirement.trim().match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/u)?.[1]; +} + +async function readRequirementsFile(path: string): Promise { + const text = await readFile(path, "utf8"); + return text + .replace(/\r\n/gu, "\n") + .replace(/\r/gu, "\n") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith("#")) + .map((line) => line.replace(/\s+#.*$/u, "").trim()) + .filter((line) => line.length > 0 && !line.startsWith("-") && !/^git\+|^https?:\/\//iu.test(line)); +} + +function stripArchiveExtension(filename: string): string { + return filename + .replace(/\.tar\.gz$/iu, "") + .replace(/\.tar\.bz2$/iu, "") + .replace(/\.tar\.xz$/iu, "") + .replace(/\.zip$/iu, "") + .replace(/\.whl$/iu, ""); +} + +function projectPrefixes(packageName: ManagedPythonPackageName): string[] { + return Array.from(new Set([ + packageName, + packageName.replace(/[-.]+/gu, "_"), + normalizeProjectName(packageName), + ])).sort((left, right) => right.length - left.length); +} + +function versionFromFilename(packageName: ManagedPythonPackageName, filename: string): string | undefined { + const basename = stripArchiveExtension(filename); + const lowerBasename = basename.toLowerCase(); + + for (const prefix of projectPrefixes(packageName)) { + const marker = `${prefix.toLowerCase()}-`; + if (!lowerBasename.startsWith(marker)) { + continue; + } + + const remainder = basename.slice(marker.length); + return filename.toLowerCase().endsWith(".whl") ? remainder.split("-")[0] : remainder; + } + + return undefined; +} + +function mergeUploadTime( + current: PythonPackageVersion | undefined, + version: string, + upload?: { uploadedAt?: string; uploadedAtMs?: number }, +): PythonPackageVersion { + if (!current) { + return versionEntry(version, upload); + } + + if (upload?.uploadedAtMs !== undefined && (current.uploadedAtMs === undefined || upload.uploadedAtMs > current.uploadedAtMs)) { + return { + ...current, + uploadedAt: upload.uploadedAt, + uploadedAtMs: upload.uploadedAtMs, + }; + } + + return current; +} + +function buildVersionListFromMap(versions: Map): PythonPackageVersion[] { + return sortVersions(Array.from(versions.values())); +} + +function parseSimpleJson(packageName: ManagedPythonPackageName, data: SimpleProjectJson): PythonPackageVersion[] { + const versions = new Map(); + + if (Array.isArray(data.versions)) { + for (const version of data.versions) { + if (typeof version === "string" && version.length > 0) { + versions.set(version, versionEntry(version)); + } + } + } + + for (const file of data.files ?? []) { + if (isYanked(file.yanked) || typeof file.filename !== "string") { + continue; + } + + const version = versionFromFilename(packageName, file.filename); + if (!version) { + continue; + } + + versions.set(version, mergeUploadTime(versions.get(version), version, uploadTime(file["upload-time"]))); + } + + return buildVersionListFromMap(versions); +} + +function decodeHtml(value: string): string { + return value + .replace(/&/gu, "&") + .replace(/"/gu, "\"") + .replace(/'/gu, "'") + .replace(/</gu, "<") + .replace(/>/gu, ">"); +} + +function attributeValue(attrs: string, name: string): string | undefined { + const pattern = new RegExp(`${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "iu"); + const match = pattern.exec(attrs); + return match ? decodeHtml(match[1] ?? match[2] ?? match[3] ?? "") : undefined; +} + +function filenameFromUrl(rawUrl: string): string | undefined { + const withoutFragment = rawUrl.split("#")[0]?.split("?")[0]; + const rawName = withoutFragment?.split("/").filter(Boolean).pop(); + if (!rawName) { + return undefined; + } + + try { + return decodeURIComponent(rawName); + } catch { + return rawName; + } +} + +function parseSimpleHtml(packageName: ManagedPythonPackageName, html: string): PythonPackageVersion[] { + const versions = new Map(); + const anchorPattern = /]*)>([\s\S]*?)<\/a>/giu; + let match: RegExpExecArray | null; + + while ((match = anchorPattern.exec(html)) !== null) { + const href = attributeValue(match[1] ?? "", "href"); + const filename = href ? filenameFromUrl(href) : decodeHtml((match[2] ?? "").replace(/<[^>]*>/gu, "").trim()); + if (!filename) { + continue; + } + + const version = versionFromFilename(packageName, filename); + if (!version) { + continue; + } + + versions.set(version, mergeUploadTime(versions.get(version), version)); + } + + return buildVersionListFromMap(versions); +} + +function mergeVersionLists(primary: PythonPackageVersion[], supplemental: PythonPackageVersion[]): PythonPackageVersion[] { + const supplementalByVersion = new Map(supplemental.map((version) => [version.version, version])); + + return sortVersions( + primary.map((version) => { + const extra = supplementalByVersion.get(version.version); + if (!extra?.uploadedAtMs || version.uploadedAtMs) { + return version; + } + + return { + ...version, + uploadedAt: extra.uploadedAt, + uploadedAtMs: extra.uploadedAtMs, + }; + }), + ); +} + +async function fetchText(url: string): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(url, { + headers: { Accept: SIMPLE_ACCEPT }, + signal: controller.signal, + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + + return { + contentType: response.headers.get("content-type") ?? "", + text: await response.text(), + }; + } finally { + clearTimeout(timeout); + } +} + +function parseSimpleResponse( + packageName: ManagedPythonPackageName, + response: FetchTextResult, +): PythonPackageVersion[] { + if (response.contentType.toLowerCase().includes("json")) { + return parseSimpleJson(packageName, JSON.parse(response.text) as SimpleProjectJson); + } + + return parseSimpleHtml(packageName, response.text); +} + +async function fetchSimpleVersions(packageName: ManagedPythonPackageName, indexUrl: string): Promise { + const response = await fetchText(`${indexUrl}/${packageName}/`); + return parseSimpleResponse(packageName, response); +} + +function hasMissingUploadTimes(versions: PythonPackageVersion[]): boolean { + return versions.some((version) => version.uploadedAtMs === undefined); +} + +export class PythonDependencyManager { + private startupUpgradePromise?: Promise; + private startupUpgradeAbort?: AbortController; + private startupUpgradeChild?: ChildProcessWithoutNullStreams; + + constructor( + private readonly paths: RuntimePaths, + private readonly initManager: InitManager, + ) {} + + getOverridesRoot(): string { + return this.paths.pythonOverridesRoot; + } + + getSourcePreset(): PythonPackageSourcePreset { + try { + const raw = JSON.parse(readFileSync(this.sourceConfigPath(), "utf8")) as { preset?: unknown }; + return this.normalizeSourcePreset(raw.preset); + } catch { + return "tuna"; + } + } + + async saveSourcePreset(preset: PythonPackageSourcePreset): Promise { + const nextPreset = this.normalizeSourcePreset(preset); + const path = this.sourceConfigPath(); + await mkdir(dirname(path), { recursive: true }); + await writeFile( + path, + `${JSON.stringify({ version: 1, preset: nextPreset, updatedAt: Date.now() }, null, 2)}\n`, + "utf8", + ); + return this.getState(); + } + + getState(): PythonOverridesState { + const sourcePreset = this.getSourcePreset(); + return { + root: this.getOverridesRoot(), + sourcePreset, + sourceUrl: this.getPrimaryIndex().url, + sourceOptions: PIP_INDEXES.map(({ preset, label, url }) => ({ preset, label, url })), + packages: MANAGED_PACKAGES, + }; + } + + buildPythonPathEnv(baseEnv: Record = process.env): Record { + const overridesRoot = this.getOverridesRoot(); + return { + PYTHONPATH: [overridesRoot, baseEnv.PYTHONPATH].filter(Boolean).join(delimiter), + [PYTHON_OVERLAY_TARGET_ENV]: overridesRoot, + }; + } + + async listVersions(packageName: ManagedPythonPackageName): Promise { + assertManagedPackage(packageName); + const primaryIndex = this.getPrimaryIndex(); + const primaryUrl = `${primaryIndex.url}/${packageName}/`; + const output = [`GET ${primaryUrl}`]; + + try { + let versions = await fetchSimpleVersions(packageName, primaryIndex.url); + output.push(`从 ${primaryIndex.label} Simple 索引解析到 ${versions.length} 个版本`); + if (versions.length === 0) { + throw new Error(`${primaryIndex.label} Simple 索引没有返回可解析的版本`); + } + + if (hasMissingUploadTimes(versions)) { + output.push(`娓呭崕 Simple 绱㈠紩缂哄皯閮ㄥ垎鍙戝竷鏃堕棿锛屽皾璇曚粠 ${PYPI_SIMPLE_INDEX}/${packageName}/ 琛ラ綈鎺掑簭淇℃伅`); + try { + const supplemental = await fetchSimpleVersions(packageName, PYPI_SIMPLE_INDEX); + versions = mergeVersionLists(versions, supplemental); + output.push("发布时间补齐完成,仍以清华源作为安装源"); + } catch (metadataError) { + output.push(`鍙戝竷鏃堕棿琛ラ綈澶辫触锛屽皢鎸夊彲鐢ㄦ椂闂翠笌鐗堟湰鍙锋帓搴? ${toDetail(metadataError)}`); + } + } + + output.push( + hasMissingUploadTimes(versions) + ? `鎵惧埌 ${versions.length} 涓増鏈紝宸叉寜鍙敤鍙戝竷鏃堕棿闄嶅簭鎺掑垪锛涚己澶卞彂甯冩椂闂寸殑鐗堟湰鐢ㄧ増鏈彿琛ヤ綅` + : `鎵惧埌 ${versions.length} 涓増鏈紝宸叉寜鍙戝竷鏃堕棿闄嶅簭鎺掑垪`, + ); + return { + packageName, + sourceUrl: primaryIndex.url, + versions, + output, + fetchedAt: Date.now(), + }; + } catch (error) { + output.push(`璇诲彇鐗堟湰鍒楄〃澶辫触: ${toDetail(error)}`); + throw new Error(output.join("\n")); + } + } + + async installVersion(request: PythonPackageInstallRequest): Promise { + assertManagedPackage(request.packageName); + if (!request.version.trim()) { + throw new Error("璇烽€夋嫨瑕佸畨瑁呯殑鐗堟湰"); + } + + const targetDir = this.getOverridesRoot(); + await mkdir(targetDir, { recursive: true }); + await this.removeOverlayPackage(request.packageName, targetDir); + + const requirement = `${request.packageName}==${request.version.trim()}`; + const baseArgs = [ + "-m", + "pip", + "install", + "--pre", + "--upgrade", + "--target", + targetDir, + "--timeout", + "120", + "--retries", + "5", + "--no-deps", + "--no-compile", + "--no-warn-script-location", + ]; + const result = await this.runPipInstallWithFallback(baseArgs, [requirement]); + + return { + packageName: request.packageName, + version: request.version.trim(), + sourceUrl: result.sourceUrl, + targetDir, + output: result.output, + installedAt: Date.now(), + }; + } + + async upgradeStartupDependencies(onOutput?: PythonOutputHandler): Promise { + if (!this.startupUpgradePromise) { + const controller = new AbortController(); + this.startupUpgradeAbort = controller; + this.startupUpgradePromise = this.installProjectDeclaredDependencies(controller.signal, onOutput).finally(() => { + if (this.startupUpgradeAbort === controller) { + this.startupUpgradeAbort = undefined; + } + this.startupUpgradePromise = undefined; + }); + } + return this.startupUpgradePromise; + } + + cancelStartupUpgrade(): boolean { + if (!this.startupUpgradeAbort || this.startupUpgradeAbort.signal.aborted) { + return false; + } + + const child = this.startupUpgradeChild; + this.startupUpgradeAbort.abort(); + if (child?.pid) { + if (process.platform === "win32") { + execFile("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true }, () => undefined); + } else { + child.kill("SIGKILL"); + } + } + return true; + } + + private async installProjectDeclaredDependencies( + signal?: AbortSignal, + onOutput?: PythonOutputHandler, + ): Promise { + const maibotRoot = this.paths.maibotRoot; + const requirementsPath = join(maibotRoot, "requirements.txt"); + const pyprojectPath = join(maibotRoot, "pyproject.toml"); + const pyprojectDependencies = existsSync(pyprojectPath) + ? await this.readPyprojectDependencies(pyprojectPath).catch(() => []) + : []; + const sourceFile = pyprojectDependencies.length > 0 + ? pyprojectPath + : existsSync(requirementsPath) + ? requirementsPath + : existsSync(pyprojectPath) + ? pyprojectPath + : undefined; + + if (!sourceFile) { + throw new Error(`鏈壘鍒?MaiBot 渚濊禆澹版槑鏂囦欢: ${requirementsPath} 鎴?${pyprojectPath}`); + } + if (sourceFile === pyprojectPath && pyprojectDependencies.length === 0) { + throw new Error(`MaiBot pyproject.toml 娌℃湁鍙敤鐨?[project.dependencies]: ${pyprojectPath}`); + } + + if (pyprojectDependencies.length > 0) { + onOutput?.(`using pyproject dependencies (${pyprojectDependencies.length} entries)`); + } + + const sourceDependencies = pyprojectDependencies.length > 0 + ? pyprojectDependencies + : await readRequirementsFile(requirementsPath); + let unsatisfied: UnsatisfiedDependency[]; + try { + unsatisfied = pyprojectDependencies.length > 0 + ? await this.getUnsatisfiedDependencySpecifiers(pyprojectDependencies) + : await this.getUnsatisfiedRequirements(requirementsPath); + } catch (error) { + onOutput?.(`dependency probe failed; installing declared dependencies: ${toDetail(error)}`); + unsatisfied = sourceDependencies.map((requirement) => ({ + requirement, + reason: "probe failed; install declared dependency", + })); + } + if (unsatisfied.length === 0) { + const output = ["all declared requirements are already satisfied in Python runtime + overrides"]; + for (const line of output) { + onOutput?.(line); + } + return { + sourceFile, + sourceUrl: TUNA_SIMPLE_INDEX, + targetDir: this.getOverridesRoot(), + output, + installedAt: Date.now(), + }; + } + for (const item of unsatisfied) { + onOutput?.(`dependency needs install: ${item.reason}`); + } + + const targetDir = this.getOverridesRoot(); + await mkdir(targetDir, { recursive: true }); + const sourceArgs = unsatisfied.map((item) => item.requirement); + await this.removeOverlayPackages(sourceArgs, targetDir); + + const baseArgs = [ + "-m", + "pip", + "install", + "--upgrade", + "--upgrade-strategy", + "only-if-needed", + "--target", + targetDir, + "--timeout", + "120", + "--retries", + "5", + "--disable-pip-version-check", + "--no-compile", + "--no-warn-script-location", + "--progress-bar", + "off", + ]; + const result = await this.runPipInstallWithFallback( + baseArgs, + sourceArgs, + signal, + onOutput, + this.buildPythonPathEnv(), + ); + + return { + sourceFile, + sourceUrl: result.sourceUrl, + targetDir, + output: result.output, + installedAt: Date.now(), + }; + } + + private async removeOverlayPackages(requirements: string[], targetDir = this.getOverridesRoot()): Promise { + const packageNames = requirements + .map(packageNameFromRequirement) + .filter((name): name is string => Boolean(name)); + + await Promise.all( + Array.from(new Set(packageNames.map(normalizeProjectName))).map((name) => this.removeOverlayPackage(name, targetDir)), + ); + } + + private async removeOverlayPackage(packageName: string, targetDir = this.getOverridesRoot()): Promise { + const normalizedName = normalizeProjectName(packageName); + const importName = packageImportName(packageName); + let entries; + try { + entries = await readdir(targetDir, { withFileTypes: true }); + } catch { + return; + } + + await Promise.all( + entries + .filter((entry) => entry.isDirectory()) + .filter((entry) => { + const normalizedEntryName = normalizeProjectName(entry.name); + const importEntryName = entry.name.toLowerCase().replace(/[-.]+/gu, "_"); + return ( + importEntryName === importName + || (normalizedEntryName.startsWith(`${normalizedName}-`) && normalizedEntryName.endsWith("-dist-info")) + || (normalizedEntryName.startsWith(`${normalizedName}-`) && normalizedEntryName.endsWith("-egg-info")) + ); + }) + .map((entry) => rm(join(targetDir, entry.name), { recursive: true, force: true })), + ); + } + + private async getUnsatisfiedRequirements(requirementsPath: string): Promise { + const script = String.raw` +import importlib.metadata as metadata +import json +import pathlib +import re +import sys +from packaging.requirements import Requirement + +path = pathlib.Path(sys.argv[1]) +missing = [] +for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or line.startswith(("-", "git+", "http://", "https://")): + continue + line = re.sub(r"\s+#.*$", "", line) + try: + requirement = Requirement(line) + except Exception: + missing.append({"requirement": line, "reason": f"unparsed requirement: {line}"}) + continue + if requirement.marker is not None and not requirement.marker.evaluate(): + continue + try: + version = metadata.version(requirement.name) + except metadata.PackageNotFoundError: + missing.append({"requirement": line, "reason": f"missing: {requirement.name}"}) + continue + if requirement.specifier and not requirement.specifier.contains(version, prereleases=True): + missing.append({"requirement": line, "reason": f"version mismatch: {requirement.name} {version} not in {requirement.specifier}"}) + +print(json.dumps(missing, ensure_ascii=False)) +`; + + try { + const output = await this.runPython(["-c", script, requirementsPath], undefined, undefined, this.buildPythonPathEnv()); + return this.parseUnsatisfiedDependencies(output); + } catch (error) { + throw new Error(`检查 MaiBot requirements.txt 失败: ${toDetail(error)}`); + } + } + + private async readPyprojectDependencies(pyprojectPath: string): Promise { + const script = String.raw` +import json +import pathlib +import sys + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib + +data = tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) +dependencies = data.get("project", {}).get("dependencies", []) +if not isinstance(dependencies, list): + dependencies = [] +print(json.dumps([item for item in dependencies if isinstance(item, str) and item.strip()])) +`; + const output = await this.runPython(["-c", script, pyprojectPath]); + const raw = output.find((line) => line.trim().startsWith("[")) ?? "[]"; + const parsed = JSON.parse(raw) as unknown; + return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : []; + } + + private async getUnsatisfiedDependencySpecifiers(dependencies: string[]): Promise { + const script = String.raw` +import importlib.metadata as metadata +import json +import sys +from packaging.requirements import Requirement + +missing = [] +for line in json.loads(sys.argv[1]): + try: + requirement = Requirement(line) + except Exception: + missing.append({"requirement": line, "reason": f"unparsed requirement: {line}"}) + continue + if requirement.marker is not None and not requirement.marker.evaluate(): + continue + try: + version = metadata.version(requirement.name) + except metadata.PackageNotFoundError: + missing.append({"requirement": line, "reason": f"missing: {requirement.name}"}) + continue + if requirement.specifier and not requirement.specifier.contains(version, prereleases=True): + missing.append({"requirement": line, "reason": f"version mismatch: {requirement.name} {version} not in {requirement.specifier}"}) + +print(json.dumps(missing, ensure_ascii=False)) +`; + + try { + const output = await this.runPython(["-c", script, JSON.stringify(dependencies)], undefined, undefined, this.buildPythonPathEnv()); + return this.parseUnsatisfiedDependencies(output); + } catch (error) { + throw new Error(`检查 MaiBot pyproject.toml 依赖失败: ${toDetail(error)}`); + } + } + + private parseUnsatisfiedDependencies(output: string[]): UnsatisfiedDependency[] { + const raw = output.find((line) => line.trim().startsWith("[")) ?? "[]"; + const parsed = JSON.parse(raw) as unknown; + if (!Array.isArray(parsed)) { + return []; + } + + return parsed.flatMap((item): UnsatisfiedDependency[] => { + if ( + typeof item === "object" + && item !== null + && "requirement" in item + && "reason" in item + && typeof item.requirement === "string" + && typeof item.reason === "string" + && item.requirement.trim() + ) { + return [{ requirement: item.requirement, reason: item.reason }]; + } + + return []; + }); + } + + private async runPipInstallWithFallback( + baseArgs: string[], + requirements: string[], + signal?: AbortSignal, + onOutput?: PythonOutputHandler, + extraEnv?: Record, + ): Promise { + const failures: string[] = []; + + for (const index of this.getPipIndexes()) { + if (signal?.aborted) { + throw new Error("Python dependency install was cancelled"); + } + + const args = [ + ...baseArgs, + "--index-url", + index.url, + ...(index.trustedHost ? ["--trusted-host", index.trustedHost] : []), + ...requirements, + ]; + onOutput?.(`pip install using ${index.label}: ${index.url}`); + + try { + const output = await this.runPython(args, signal, onOutput, extraEnv); + return { + output, + sourceUrl: index.url, + }; + } catch (error) { + const detail = toDetail(error); + failures.push(`[${index.label}] ${detail}`); + onOutput?.(`pip install failed with ${index.label}; ${this.nextIndexHint(index.url)}`); + } + } + + throw new Error(`All Python package indexes failed:\n${failures.join("\n\n")}`); + } + + private nextIndexHint(failedUrl: string): string { + const indexes = this.getPipIndexes(); + const currentIndex = indexes.findIndex((item) => item.url === failedUrl); + const next = indexes[currentIndex + 1]; + return next ? `retrying with ${next.label}` : "no fallback index remains"; + } + + private sourceConfigPath(): string { + return join(this.paths.userDataRoot, PYTHON_SOURCE_FILE); + } + + private normalizeSourcePreset(value: unknown): PythonPackageSourcePreset { + return value === "pypi" || value === "aliyun" || value === "tuna" ? value : "tuna"; + } + + private getPrimaryIndex(): typeof PIP_INDEXES[number] { + const preset = this.getSourcePreset(); + return PIP_INDEXES.find((index) => index.preset === preset) ?? PIP_INDEXES[0]; + } + + private getPipIndexes(): typeof PIP_INDEXES { + const primary = this.getPrimaryIndex(); + return [primary, ...PIP_INDEXES.filter((index) => index.preset !== primary.preset)] as typeof PIP_INDEXES; + } + + private runPython( + args: string[], + signal?: AbortSignal, + onOutput?: PythonOutputHandler, + extraEnv?: Record, + ): Promise { + if (onOutput) { + return this.runPythonStreaming(args, signal, onOutput, extraEnv); + } + + return new Promise((resolve, reject) => { + execFile( + this.initManager.getPythonPath(), + args, + { + cwd: this.paths.installRoot, + timeout: PIP_TIMEOUT_MS, + windowsHide: true, + maxBuffer: 8 * 1024 * 1024, + signal, + env: { + ...process.env, + ...extraEnv, + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + }, + }, + (error, stdout, stderr) => { + const output = splitOutput(`${stdout}${stderr}`); + if (error) { + reject(new Error(output.join("\n") || toDetail(error))); + return; + } + + resolve(output); + }, + ); + }); + } + + private runPythonStreaming( + args: string[], + signal: AbortSignal | undefined, + onOutput: PythonOutputHandler, + extraEnv?: Record, + ): Promise { + return new Promise((resolve, reject) => { + const output: string[] = []; + const child = spawn(this.initManager.getPythonPath(), args, { + cwd: this.paths.installRoot, + windowsHide: true, + signal, + env: { + ...process.env, + ...extraEnv, + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + }, + }); + this.startupUpgradeChild = child; + let settled = false; + let stdoutBuffer = ""; + let stderrBuffer = ""; + const timeout = setTimeout(() => { + child.kill(); + finish(new Error(`Python command timed out after ${Math.round(PIP_TIMEOUT_MS / 1000)}s`)); + }, PIP_TIMEOUT_MS); + + const emitLine = (line: string): void => { + const normalized = line.trimEnd(); + if (!normalized) { + return; + } + output.push(normalized); + onOutput(normalized); + }; + + const collect = (chunk: Buffer, stream: "stdout" | "stderr"): void => { + const current = stream === "stdout" ? stdoutBuffer : stderrBuffer; + const parts = `${current}${chunk.toString("utf8")}`.replace(/\r(?!\n)/gu, "\n").split(/\n/u); + const nextBuffer = parts.pop() ?? ""; + for (const part of parts) { + emitLine(part); + if (/Installing collected packages/iu.test(part)) { + emitLine("pip is writing package files; this step can be quiet for a while"); + } + } + if (stream === "stdout") { + stdoutBuffer = nextBuffer; + } else { + stderrBuffer = nextBuffer; + } + }; + + const flush = (): void => { + emitLine(stdoutBuffer); + emitLine(stderrBuffer); + stdoutBuffer = ""; + stderrBuffer = ""; + }; + + const finish = (error?: Error): void => { + if (settled) { + return; + } + settled = true; + if (this.startupUpgradeChild === child) { + this.startupUpgradeChild = undefined; + } + clearTimeout(timeout); + flush(); + if (error) { + reject(error); + return; + } + resolve(output); + }; + + child.stdout?.on("data", (chunk: Buffer) => collect(chunk, "stdout")); + child.stderr?.on("data", (chunk: Buffer) => collect(chunk, "stderr")); + child.on("error", (error) => finish(error)); + child.on("close", (code, signalName) => { + if (code === 0) { + finish(); + return; + } + + const detail = output.join("\n") || `Python command exited with code ${code ?? "null"} signal ${signalName ?? "null"}`; + finish(new Error(detail)); + }); + }); + } +} + diff --git a/src/main/services/resource-location-manager.ts b/src/main/services/resource-location-manager.ts new file mode 100644 index 0000000..04d3548 --- /dev/null +++ b/src/main/services/resource-location-manager.ts @@ -0,0 +1,399 @@ +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { cp, mkdir, unlink, writeFile } from "node:fs/promises"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import type { + RuntimePaths, + RuntimeResourcePathChangeResult, + RuntimeResourcePathConfig, + RuntimeResourcePathKey, +} from "../../shared/contracts"; +import { applyRuntimeResourcePaths, LEGACY_RESOURCE_LOCATION_FILE, RESOURCE_PATHS_FILE } from "./paths"; + +type RuntimeResourcePathMap = Record; + +interface ResourceLockPayload { + pid: number; + installRoot: string; + key: RuntimeResourcePathKey; + path: string; + startedAt: number; +} + +interface ResourceLock { + lockPath: string; + release: () => void; +} + +interface ResourceLockAcquireResult { + acquired: boolean; + lock?: ResourceLock; + existing?: ResourceLockPayload; +} + +interface StoredResourcePathsFile { + version: 1; + paths: Partial; + updatedAt: number; +} + +const RESOURCE_LOCK_FILE = "resource.lock"; +const RESOURCE_KEYS: RuntimeResourcePathKey[] = ["maibot", "napcat", "pythonOverrides"]; +const EDITABLE_RESOURCE_KEYS: RuntimeResourcePathKey[] = ["maibot", "napcat"]; + +function normalizePathForCompare(path: string): string { + const resolved = resolve(path); + return process.platform === "win32" ? resolved.toLowerCase() : resolved; +} + +function samePath(left: string, right: string): boolean { + return normalizePathForCompare(left) === normalizePathForCompare(right); +} + +function isPathInside(parent: string, child: string): boolean { + const resolvedParent = resolve(parent); + const resolvedChild = resolve(child); + const diff = relative(resolvedParent, resolvedChild); + return Boolean(diff) && diff !== ".." && !diff.startsWith(`..${sep}`) && !isAbsolute(diff); +} + +function isPathNestedEitherWay(left: string, right: string): boolean { + return isPathInside(left, right) || isPathInside(right, left); +} + +function isProcessAlive(pid: number | undefined): boolean { + if (!pid || pid === process.pid) { + return false; + } + + try { + process.kill(pid, 0); + return true; + } catch (error) { + return (error as NodeJS.ErrnoException).code === "EPERM"; + } +} + +function readLockPayload(lockPath: string): ResourceLockPayload | undefined { + try { + return JSON.parse(readFileSync(lockPath, "utf8")) as ResourceLockPayload; + } catch { + return undefined; + } +} + +function labelForKey(key: RuntimeResourcePathKey): string { + switch (key) { + case "maibot": + return "MaiBot路径"; + case "napcat": + return "NapCat路径"; + case "pythonOverrides": + return "python可写环境"; + } +} + +export class ResourceLocationManager { + private locks: ResourceLock[] = []; + + constructor( + private readonly paths: RuntimePaths, + private readonly lockEnabled: boolean, + ) {} + + getPathConfigs(): RuntimeResourcePathConfig[] { + return EDITABLE_RESOURCE_KEYS.map((key) => { + const value = this.getPath(key); + const defaultValue = this.getDefaultPath(key); + return { + key, + label: labelForKey(key), + value, + defaultValue, + customized: !samePath(value, defaultValue), + }; + }); + } + + acquireInitialLock(): ResourceLockAcquireResult { + this.assertUniquePathSet(this.currentPathSet()); + if (!this.lockEnabled) { + return { acquired: true }; + } + + const acquired: ResourceLock[] = []; + for (const key of RESOURCE_KEYS) { + const result = this.acquireLockForPath(key, this.getPath(key)); + if (!result.acquired) { + for (const lock of acquired) { + lock.release(); + } + return result; + } + if (result.lock) { + acquired.push(result.lock); + } + } + + this.release(); + this.locks = acquired; + return { acquired: true }; + } + + release(): void { + for (const lock of this.locks) { + lock.release(); + } + this.locks = []; + } + + async migratePath(key: RuntimeResourcePathKey, targetPath: string): Promise { + return this.changePath(key, targetPath, true); + } + + async selectPath(key: RuntimeResourcePathKey, targetPath: string): Promise { + return this.changePath(key, targetPath, false); + } + + async resetPath(key: RuntimeResourcePathKey): Promise { + return this.changePath(key, this.getDefaultPath(key), false); + } + + private async changePath( + key: RuntimeResourcePathKey, + targetPath: string, + copyExisting: boolean, + ): Promise { + if (key === "pythonOverrides") { + throw new Error("python可写环境路径不允许修改"); + } + const normalizedTarget = this.normalizeTargetPath(targetPath); + const previousPath = this.getPath(key); + const pathChanged = !samePath(previousPath, normalizedTarget); + const nextPathSet = { ...this.currentPathSet(), [key]: normalizedTarget }; + const copiedEntries: string[] = []; + + this.assertCanUsePathSet(key, nextPathSet); + const nextLockResult: ResourceLockAcquireResult = this.lockEnabled && pathChanged + ? this.acquireLockForPath(key, normalizedTarget) + : { acquired: true }; + if (!nextLockResult.acquired) { + throw new Error(`目标路径正在被其他进程使用: ${nextLockResult.existing?.pid ?? "unknown"}`); + } + + try { + await mkdir(normalizedTarget, { recursive: true }); + if (copyExisting && pathChanged) { + copiedEntries.push(...(await this.copyRuntimeResourceEntry(key, previousPath, normalizedTarget))); + } + await this.writePathSet(nextPathSet); + applyRuntimeResourcePaths(this.paths, nextPathSet); + if (pathChanged) { + this.replaceLock(key, nextLockResult.lock); + } + } catch (error) { + nextLockResult.lock?.release(); + throw error; + } + + return { + key, + previousPath, + path: this.getPath(key), + defaultPath: this.getDefaultPath(key), + copiedEntries, + changedAt: Date.now(), + }; + } + + private normalizeTargetPath(targetPath: string): string { + const trimmed = targetPath.trim(); + if (!trimmed) { + throw new Error("路径不能为空"); + } + return resolve(trimmed); + } + + private assertCanUsePathSet(changedKey: RuntimeResourcePathKey, pathSet: RuntimeResourcePathMap): void { + this.assertUniquePathSet(pathSet); + + const targetPath = pathSet[changedKey]; + const defaultPath = this.getDefaultPath(changedKey); + const movingToDefault = samePath(targetPath, defaultPath); + if (movingToDefault) { + return; + } + + if (changedKey !== "pythonOverrides" && isPathNestedEitherWay(this.paths.bundledModulesRoot, targetPath)) { + throw new Error("MaiBot 与 NapCat 路径不能放在一键包内置 modules 模板目录中"); + } + if (changedKey === "pythonOverrides" && isPathNestedEitherWay(this.paths.runtimeRoot, targetPath)) { + throw new Error("python可写环境不能放在 python基础环境目录中"); + } + } + + private assertUniquePathSet(pathSet: RuntimeResourcePathMap): void { + for (let index = 0; index < RESOURCE_KEYS.length; index += 1) { + const leftKey = RESOURCE_KEYS[index]; + const leftPath = pathSet[leftKey]; + for (const rightKey of RESOURCE_KEYS.slice(index + 1)) { + const rightPath = pathSet[rightKey]; + if (samePath(leftPath, rightPath) || isPathNestedEitherWay(leftPath, rightPath)) { + throw new Error(`${labelForKey(leftKey)} 与 ${labelForKey(rightKey)} 必须使用彼此独立的目录`); + } + } + } + } + + private async copyRuntimeResourceEntry( + key: RuntimeResourcePathKey, + sourcePath: string, + targetPath: string, + ): Promise { + const copiedEntries: string[] = []; + if (existsSync(sourcePath)) { + await mkdir(dirname(targetPath), { recursive: true }); + await cp(sourcePath, targetPath, { + recursive: true, + force: false, + errorOnExist: false, + preserveTimestamps: true, + }); + copiedEntries.push(key); + } + + if (key === "napcat") { + const sourceFramework = join(dirname(sourcePath), "napcatframework"); + const targetFramework = join(dirname(targetPath), "napcatframework"); + if (existsSync(sourceFramework) && !samePath(sourceFramework, targetFramework)) { + await mkdir(dirname(targetFramework), { recursive: true }); + await cp(sourceFramework, targetFramework, { + recursive: true, + force: false, + errorOnExist: false, + preserveTimestamps: true, + }); + copiedEntries.push("napcatframework"); + } + } + + return copiedEntries; + } + + private currentPathSet(): RuntimeResourcePathMap { + return { + maibot: this.paths.maibotRoot, + napcat: this.paths.napcatRoot, + pythonOverrides: this.paths.pythonOverridesRoot, + }; + } + + private getPath(key: RuntimeResourcePathKey): string { + return this.currentPathSet()[key]; + } + + private getDefaultPath(key: RuntimeResourcePathKey): string { + switch (key) { + case "maibot": + return this.paths.defaultMaibotRoot; + case "napcat": + return this.paths.defaultNapcatRoot; + case "pythonOverrides": + return this.paths.defaultPythonOverridesRoot; + } + } + + private locationPath(): string { + return join(this.paths.userDataRoot, RESOURCE_PATHS_FILE); + } + + private legacyLocationPath(): string { + return join(this.paths.userDataRoot, LEGACY_RESOURCE_LOCATION_FILE); + } + + private async writePathSet(pathSet: RuntimeResourcePathMap): Promise { + const customizedPaths = Object.fromEntries( + RESOURCE_KEYS.filter((key) => !samePath(pathSet[key], this.getDefaultPath(key))).map((key) => [ + key, + pathSet[key], + ]), + ) as Partial; + + const storePath = this.locationPath(); + await unlink(this.legacyLocationPath()).catch(() => undefined); + if (Object.keys(customizedPaths).length === 0) { + await unlink(storePath).catch(() => undefined); + return; + } + + const payload: StoredResourcePathsFile = { + version: 1, + paths: customizedPaths, + updatedAt: Date.now(), + }; + await mkdir(dirname(storePath), { recursive: true }); + await writeFile(storePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); + } + + private acquireLockForPath(key: RuntimeResourcePathKey, resourcePath: string): ResourceLockAcquireResult { + const lockPath = join(resourcePath, RESOURCE_LOCK_FILE); + const payload: ResourceLockPayload = { + pid: process.pid, + installRoot: this.paths.installRoot, + key, + path: resourcePath, + startedAt: Date.now(), + }; + + mkdirSync(resourcePath, { recursive: true }); + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + writeFileSync(lockPath, `${JSON.stringify(payload, null, 2)}\n`, { flag: "wx" }); + return { + acquired: true, + lock: { + lockPath, + release: () => { + const current = readLockPayload(lockPath); + if (current?.pid === process.pid) { + try { + unlinkSync(lockPath); + } catch { + // The lock is best-effort; shutdown can continue if it was removed already. + } + } + }, + }, + }; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "EEXIST") { + throw error; + } + + const existing = readLockPayload(lockPath); + if (isProcessAlive(existing?.pid)) { + return { acquired: false, existing }; + } + + try { + unlinkSync(lockPath); + } catch { + return { acquired: false, existing }; + } + } + } + + return { acquired: false, existing: readLockPayload(lockPath) }; + } + + private replaceLock(key: RuntimeResourcePathKey, nextLock: ResourceLock | undefined): void { + const existingIndex = this.locks.findIndex((lock) => readLockPayload(lock.lockPath)?.key === key); + if (existingIndex >= 0) { + this.locks[existingIndex].release(); + this.locks.splice(existingIndex, 1); + } + if (nextLock) { + this.locks.push(nextLock); + } + } +} diff --git a/src/main/services/service-manager.ts b/src/main/services/service-manager.ts new file mode 100644 index 0000000..b2e6df9 --- /dev/null +++ b/src/main/services/service-manager.ts @@ -0,0 +1,1685 @@ +import { EventEmitter } from "node:events"; +import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import net from "node:net"; +import { dirname, join } from "node:path"; +import type { + PtyDataEvent, + PtyErrorEvent, + PtyExitEvent, + PtySessionSnapshot, + RuntimePaths, + RuntimePathConfig, + RuntimePathKey, + RuntimePathKind, + RuntimePathUpdate, + ServiceCommandConfig, + ServiceCommandUpdate, + ServiceDescriptor, + ServiceHealth, + ServiceId, + ServiceStatus, + TerminalMode, + TerminalSettings, +} from "../../shared/contracts"; +import type { PtySessionManager } from "../pty/pty-session-manager"; +import { InitManager } from "./init-manager"; +import { LogStore } from "./log-store"; +import { PythonDependencyManager } from "./python-dependency-manager"; + +interface ServiceDefinition { + id: ServiceId; + name: string; + port: number; + ports: number[]; + url: string; + cwd: string; + defaultRequiredPaths: string[]; + conflictPorts: number[]; + readyPorts: number[]; + buildDefaultCommand?: () => Promise; + buildDefaultCommandLine: () => Promise; + displayDefaultCommandLine?: () => Promise; +} + +interface RuntimePathDefinition { + key: RuntimePathKey; + label: string; + kind: RuntimePathKind; + defaultValue: string; +} + +interface ResolvedServiceCommand { + cwd: string; + command?: string[]; + commandLine: string; + requiredPaths: string[]; + customized: boolean; +} + +interface ServiceState { + status: ServiceStatus; + health: ServiceHealth; + managed: boolean; + pid?: number; + terminalMode?: TerminalMode; + detail?: string; + error?: string; + desired?: boolean; + restartAttempts?: number; + command?: string[]; + cwd?: string; + dynamicUrl?: string; + startedAt?: number; + stoppedAt?: number; + ptySessionId?: string; + stopTimer?: NodeJS.Timeout; + restartTimer?: NodeJS.Timeout; + healthFailures?: number; +} + +interface StoredServiceCommand { + cwd?: string; + commandLine?: string; +} + +interface StoredCommandFile { + version: 1; + services: Partial>; +} + +interface StoredRuntimePathFile { + version: 1; + paths: Partial>; +} + +interface StoredTerminalSettingsFile { + version: 1; + useEmbeddedTerminal?: boolean; + fontSize?: number; +} + +const STOP_FORCE_AFTER_MS = 10_000; +const WATCHDOG_INTERVAL_MS = 5_000; +const MAX_RESTART_ATTEMPTS = 3; +const RESTART_DELAY_MS = 2_500; +const SERVICE_TERMINAL_COLS = 120; +const SERVICE_TERMINAL_ROWS = 36; +const COMMAND_CONFIG_FILE = "service-commands.json"; +const RUNTIME_PATH_CONFIG_FILE = "runtime-paths.json"; +const TERMINAL_SETTINGS_FILE = "terminal-settings.json"; +const SERVICE_IDS: ServiceId[] = ["maibot", "napcat"]; +const DEFAULT_TERMINAL_SETTINGS: TerminalSettings = { + useEmbeddedTerminal: true, + fontSize: 12, +}; +const MIN_TERMINAL_FONT_SIZE = 10; +const MAX_TERMINAL_FONT_SIZE = 22; + +function quoteCommandPart(value: string): string { + const normalized = normalizePathLikeValue(value); + if (!/[ \t&()^|<>"]/u.test(normalized)) { + return normalized; + } + + return `"${normalized.replace(/"/g, '\\"')}"`; +} + +function normalizePathLikeValue(value: string): string { + let normalized = value.trim(); + normalized = normalized.replace(/^\\(["'])/u, "$1").replace(/\\(["'])$/u, "$1"); + + const first = normalized[0]; + const last = normalized[normalized.length - 1]; + if ((first === `"` && last === `"`) || (first === `'` && last === `'`)) { + normalized = normalized.slice(1, -1).trim(); + } + + return normalized; +} + +function normalizeCommandLine(value: string): string { + return value + .trim() + .replace(/(^|\s)\\(["'])/gu, "$1$2") + .replace(/\\(["'])(?=\s|$)/gu, "$1"); +} + +function normalizePathSeparators(value: string): string { + return normalizePathLikeValue(value) + .replace(/\\/gu, "/") + .replace(/\/+/gu, "/") + .replace(/\/$/u, ""); +} + +function normalizePathForMatch(value: string): string { + const normalized = normalizePathSeparators(value); + return process.platform === "win32" ? normalized.toLowerCase() : normalized; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function replaceAllPathVariants(value: string, search: string, replacement: string): string { + const flags = process.platform === "win32" ? "giu" : "gu"; + const variants = new Set([ + search, + search.replace(/\\/gu, "/"), + search.replace(/\//gu, "\\"), + ]); + + let nextValue = value; + for (const variant of variants) { + if (!variant) { + continue; + } + + nextValue = nextValue.replace(new RegExp(escapeRegExp(variant), flags), replacement); + } + + return nextValue; +} + +function relocateBundledModulePath(value: string, paths: RuntimePaths): string { + const normalized = normalizePathLikeValue(value); + const normalizedWithSlashes = normalizePathSeparators(normalized); + const valueForMatch = normalizePathForMatch(normalized); + const mappings = [ + { source: join(paths.bundledModulesRoot, "MaiBot"), target: paths.maibotRoot }, + { source: join(paths.bundledModulesRoot, "napcat"), target: paths.napcatRoot }, + { source: join(paths.bundledModulesRoot, "SnowLuma"), target: paths.snowlumaRoot }, + { source: join(paths.bundledModulesRoot, "napcatframework"), target: join(dirname(paths.napcatRoot), "napcatframework") }, + ]; + + for (const mapping of mappings) { + const sourceWithSlashes = normalizePathSeparators(mapping.source); + const sourceForMatch = normalizePathForMatch(mapping.source); + const isBundledRoot = valueForMatch === sourceForMatch; + const isBundledChild = valueForMatch.startsWith(`${sourceForMatch}/`); + if (!isBundledRoot && !isBundledChild) { + continue; + } + + const suffix = normalizedWithSlashes.slice(sourceWithSlashes.length); + const suffixParts = suffix.split("/").filter(Boolean); + return join(mapping.target, ...suffixParts); + } + + return normalized; +} + +function relocateBundledModuleReferences(value: string, paths: RuntimePaths): string { + return [ + [join(paths.bundledModulesRoot, "MaiBot"), paths.maibotRoot], + [join(paths.bundledModulesRoot, "napcat"), paths.napcatRoot], + [join(paths.bundledModulesRoot, "SnowLuma"), paths.snowlumaRoot], + [join(paths.bundledModulesRoot, "napcatframework"), join(dirname(paths.napcatRoot), "napcatframework")], + ].reduce((nextValue, [search, replacement]) => replaceAllPathVariants(nextValue, search, replacement), value); +} + +function extractLeadingExecutablePath(commandLine: string): string | undefined { + const trimmed = commandLine.trim(); + if (!trimmed) { + return undefined; + } + + const quoted = trimmed.match(/^"([^"]+)"/u) ?? trimmed.match(/^'([^']+)'/u); + const candidate = quoted?.[1] ?? trimmed.split(/\s+/u)[0]; + if (!candidate) { + return undefined; + } + + const looksLikePath = + /^[a-zA-Z]:[\\/]/u.test(candidate) || + /^\\\\/u.test(candidate) || + candidate.includes("/") || + candidate.includes("\\"); + return looksLikePath ? candidate : undefined; +} + +function serviceSessionId(serviceId: ServiceId): string { + return `service:${serviceId}`; +} + +function serviceIdFromSession(sessionId: string): ServiceId | undefined { + const id = sessionId.replace(/^service:/u, ""); + return SERVICE_IDS.includes(id as ServiceId) ? (id as ServiceId) : undefined; +} + +function isLivePtyStatus(status: PtySessionSnapshot["status"]): boolean { + return status === "starting" || status === "running" || status === "stopping"; +} + +function createServiceEnv(extraEnv: Record | undefined): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { ...process.env }; + if (!extraEnv) { + return env; + } + + for (const [key, value] of Object.entries(extraEnv)) { + env[key] = value; + } + + return env; +} + +function killWindowsProcessTree(pid: number, force: boolean): Promise { + const args = force ? ["/F", "/T", "/PID", String(pid)] : ["/T", "/PID", String(pid)]; + return new Promise((resolve, reject) => { + const child = spawn("taskkill", args, { + windowsHide: true, + stdio: "ignore", + }); + child.once("error", reject); + child.once("exit", (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`taskkill exited with code ${code ?? "unknown"}`)); + }); + }); +} + +function probePort(port: number, host = "127.0.0.1", timeoutMs = 450): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + let settled = false; + + const finish = (result: boolean): void => { + if (settled) { + return; + } + + settled = true; + socket.destroy(); + resolve(result); + }; + + socket.setTimeout(timeoutMs); + socket.once("connect", () => finish(true)); + socket.once("timeout", () => finish(false)); + socket.once("error", () => finish(false)); + socket.connect(port, host); + }); +} + +async function waitForPort(port: number, timeoutMs = 18_000): Promise { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + if (await probePort(port)) { + return true; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + return false; +} + +class ServiceCommandStore { + private readonly path: string; + private cache: StoredCommandFile | null = null; + + constructor(private readonly paths: RuntimePaths) { + this.path = join(paths.userDataRoot, COMMAND_CONFIG_FILE); + } + + async get(serviceId: ServiceId): Promise { + const command = (await this.read()).services[serviceId]; + if (!command) { + return undefined; + } + + return { + commandLine: command.commandLine + ? relocateBundledModuleReferences(normalizeCommandLine(command.commandLine), this.paths) + : undefined, + }; + } + + async set(serviceId: ServiceId, command: StoredServiceCommand): Promise { + const file = await this.read(); + file.services[serviceId] = { + commandLine: command.commandLine + ? relocateBundledModuleReferences(normalizeCommandLine(command.commandLine), this.paths) || undefined + : undefined, + }; + await this.write(file); + } + + async reset(serviceId: ServiceId): Promise { + const file = await this.read(); + delete file.services[serviceId]; + await this.write(file); + } + + private async read(): Promise { + if (this.cache) { + return this.cache; + } + + try { + const raw = JSON.parse(await readFile(this.path, "utf8")) as StoredCommandFile; + this.cache = { + version: 1, + services: raw.services ?? {}, + }; + } catch { + this.cache = { version: 1, services: {} }; + } + + return this.cache; + } + + private async write(file: StoredCommandFile): Promise { + this.cache = file; + await mkdir(dirname(this.path), { recursive: true }); + await writeFile(this.path, `${JSON.stringify(file, null, 2)}\n`, "utf8"); + } +} + +class RuntimePathStore { + private readonly path: string; + private cache: StoredRuntimePathFile; + + constructor(private readonly paths: RuntimePaths) { + this.path = join(paths.userDataRoot, RUNTIME_PATH_CONFIG_FILE); + this.cache = this.read(); + } + + get(key: RuntimePathKey): string | undefined { + const value = this.cache.paths[key]; + return value ? relocateBundledModulePath(value, this.paths) || undefined : undefined; + } + + async set(key: RuntimePathKey, value: string): Promise { + this.cache.paths[key] = relocateBundledModulePath(value, this.paths) || undefined; + await this.write(); + } + + async reset(key: RuntimePathKey): Promise { + delete this.cache.paths[key]; + await this.write(); + } + + private read(): StoredRuntimePathFile { + try { + const raw = JSON.parse(readFileSync(this.path, "utf8")) as StoredRuntimePathFile; + return { + version: 1, + paths: raw.paths ?? {}, + }; + } catch { + return { version: 1, paths: {} }; + } + } + + private async write(): Promise { + await mkdir(dirname(this.path), { recursive: true }); + await writeFile(this.path, `${JSON.stringify(this.cache, null, 2)}\n`, "utf8"); + } +} + +class TerminalSettingsStore { + private readonly path: string; + private cache: TerminalSettings; + + constructor(paths: RuntimePaths) { + this.path = join(paths.userDataRoot, TERMINAL_SETTINGS_FILE); + this.cache = this.read(); + } + + get(): TerminalSettings { + return { ...this.cache }; + } + + async set(settings: TerminalSettings): Promise { + this.cache = { + useEmbeddedTerminal: settings.useEmbeddedTerminal !== false, + fontSize: normalizeTerminalFontSize(settings.fontSize), + }; + await mkdir(dirname(this.path), { recursive: true }); + await writeFile( + this.path, + `${JSON.stringify({ version: 1, ...this.cache } satisfies StoredTerminalSettingsFile, null, 2)}\n`, + "utf8", + ); + return this.get(); + } + + private read(): TerminalSettings { + try { + const raw = JSON.parse(readFileSync(this.path, "utf8")) as StoredTerminalSettingsFile; + return { + useEmbeddedTerminal: raw.useEmbeddedTerminal !== false, + fontSize: normalizeTerminalFontSize(raw.fontSize), + }; + } catch { + return { ...DEFAULT_TERMINAL_SETTINGS }; + } + } +} + +function normalizeTerminalFontSize(value: unknown): number { + const fontSize = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(fontSize)) { + return DEFAULT_TERMINAL_SETTINGS.fontSize; + } + return Math.max(MIN_TERMINAL_FONT_SIZE, Math.min(MAX_TERMINAL_FONT_SIZE, Math.round(fontSize))); +} + +export class ServiceManager extends EventEmitter { + private readonly states = new Map(); + private definitions: ServiceDefinition[]; + private readonly watchdogTimer: NodeJS.Timeout; + private readonly commandStore: ServiceCommandStore; + private readonly runtimePathStore: RuntimePathStore; + private readonly terminalSettingsStore: TerminalSettingsStore; + private readonly externalProcesses = new Map(); + private readonly logLineBuffers = new Map(); + + constructor( + private readonly paths: RuntimePaths, + private readonly initManager: InitManager, + private readonly logs: LogStore, + private readonly pty: PtySessionManager, + private readonly pythonDependencyManager?: PythonDependencyManager, + ) { + super(); + this.commandStore = new ServiceCommandStore(paths); + this.runtimePathStore = new RuntimePathStore(paths); + this.terminalSettingsStore = new TerminalSettingsStore(paths); + this.definitions = this.createDefinitions(); + for (const definition of this.definitions) { + this.states.set(definition.id, { + status: "stopped", + health: "unknown", + managed: false, + desired: false, + restartAttempts: 0, + healthFailures: 0, + detail: "\u7b49\u5f85\u542f\u52a8", + }); + } + + this.pty.on("data", (event) => this.handlePtyData(event)); + this.pty.on("exit", (event) => this.handlePtyExit(event)); + this.pty.on("error", (event) => this.handlePtyError(event)); + this.pty.on("snapshot", (snapshot) => this.handlePtySnapshot(snapshot)); + + this.watchdogTimer = setInterval(() => { + void this.refresh().catch((error: unknown) => { + this.logs.append("desktop", "system", `service watchdog failed: ${String(error)}`); + }); + }, WATCHDOG_INTERVAL_MS); + } + + async startAll(): Promise { + await this.initManager.assertAgreementsConfirmed(); + for (const serviceId of ["napcat", "maibot"] as ServiceId[]) { + await this.start(serviceId); + } + return this.refresh(); + } + + async stopAll(): Promise { + for (const serviceId of ["maibot", "napcat"] as ServiceId[]) { + await this.stop(serviceId); + } + return this.snapshot(); + } + + async shutdownAll(timeoutMs = STOP_FORCE_AFTER_MS + 2_000): Promise { + await this.stopAll(); + + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + const running = [...this.states.values()].some( + (state) => + (state.ptySessionId || state.terminalMode === "external") && + state.status !== "stopped" && + state.status !== "error", + ); + if (!running) { + return this.snapshot(); + } + + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + for (const serviceId of ["maibot", "napcat"] as ServiceId[]) { + await this.kill(serviceId); + } + return this.snapshot(); + } + + async restart(serviceId: ServiceId): Promise { + await this.stop(serviceId); + return this.start(serviceId); + } + + async start(serviceId: ServiceId, resetRestartAttempts = true): Promise { + const definition = this.getDefinition(serviceId); + const state = this.getState(serviceId); + const sessionId = serviceSessionId(serviceId); + const existingSession = this.pty.list().find((session) => session.id === sessionId); + + if (serviceId === "maibot") { + await this.initManager.assertAgreementsConfirmed(); + } + + if (state.status === "starting") { + return this.toDescriptor(definition, state); + } + + if (existingSession && isLivePtyStatus(existingSession.status)) { + this.setState(serviceId, { + ...state, + status: "running", + health: definition.readyPorts.length > 0 ? "checking" : "ready", + managed: true, + desired: true, + pid: existingSession.pid, + terminalMode: "embedded", + ptySessionId: existingSession.id, + detail: `已接管现有 PTY 会话,PID ${existingSession.pid ?? "未知"}`, + }); + return this.toDescriptor(definition, this.getState(serviceId)); + } + + let resolved: ResolvedServiceCommand; + try { + resolved = await this.resolveStartCommand(definition); + this.assertRequiredPaths(definition, resolved.requiredPaths); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logs.append(serviceId, "system", `start failed: ${message}`); + this.setState(serviceId, { + ...state, + status: "error", + health: "unreachable", + desired: false, + error: message, + detail: message, + managed: false, + }); + throw error; + } + + await this.assertPortsFree(definition); + + const displayCommand = resolved.command ?? [resolved.commandLine]; + const dynamicUrl = await this.resolveServiceUrl(definition.id, definition.url); + + this.setState(serviceId, { + ...state, + status: "starting", + health: "checking", + desired: true, + restartAttempts: resetRestartAttempts ? 0 : (state.restartAttempts ?? 0), + healthFailures: 0, + error: undefined, + detail: `\u6b63\u5728\u542f\u52a8 ${definition.name} PTY`, + stoppedAt: undefined, + terminalMode: this.shouldUseEmbeddedTerminal() ? "embedded" : "external", + pid: undefined, + ptySessionId: undefined, + command: displayCommand, + cwd: resolved.cwd, + dynamicUrl, + }); + + this.logs.append( + serviceId, + "system", + `start: ${resolved.commandLine} cwd=${resolved.cwd}${resolved.customized ? " customized=true" : ""}`, + ); + + try { + const useCommandLine = !resolved.command; + const agreementEnv = await this.initManager.getAgreementEnvVars(); + const usePythonOverlay = definition.id === "maibot" && !this.isCustomPythonRuntimeEnabled(); + const baseEnv = usePythonOverlay ? this.pythonDependencyManager?.buildPythonPathEnv() : undefined; + const mergedEnv: Record = { ...(baseEnv ?? {}), ...agreementEnv }; + if (usePythonOverlay && this.pythonDependencyManager) { + const syncedPythonOverrides = await this.initManager.ensureBundledPythonOverrides(); + if (syncedPythonOverrides.length > 0) { + this.logs.append("maibot", "system", `startup dependency upgrade: copied bundled Python overrides to ${syncedPythonOverrides[0]}`); + } + this.setState("maibot", { + ...this.getState("maibot"), + detail: "\u6b63\u5728\u68c0\u67e5 MaiBot \u542f\u52a8\u4f9d\u8d56\uff0c\u5b8c\u6210\u540e\u4f1a\u542f\u52a8 PTY", + }); + this.logs.append("maibot", "system", "startup dependency upgrade: checking MaiBot dependency files"); + const dependencyUpgradeStartedAt = Date.now(); + const dependencyUpgradeHeartbeat = setInterval(() => { + const elapsedSeconds = Math.round((Date.now() - dependencyUpgradeStartedAt) / 1000); + this.logs.append("maibot", "system", `startup dependency upgrade still running (${elapsedSeconds}s)`); + }, 15_000); + const upgradeResult = await this.pythonDependencyManager + .upgradeStartupDependencies((line) => { + this.logs.append("maibot", "system", `startup dependency upgrade: ${line}`); + }) + .finally(() => clearInterval(dependencyUpgradeHeartbeat)); + this.logs.append( + "maibot", + "system", + `startup dependency upgrade completed: ${upgradeResult.sourceFile} -> ${upgradeResult.targetDir}`, + ); + if (!this.getState("maibot").desired) { + return this.toDescriptor(definition, this.getState("maibot")); + } + this.setState("maibot", { + ...this.getState("maibot"), + detail: "\u4f9d\u8d56\u68c0\u67e5\u5b8c\u6210\uff0c\u6b63\u5728\u542f\u52a8 MaiBot Core PTY", + }); + } + if (!this.shouldUseEmbeddedTerminal()) { + const child = this.startExternalTerminal(definition, resolved, mergedEnv); + this.setState(serviceId, { + ...this.getState(serviceId), + status: "running", + health: definition.readyPorts.length > 0 ? "checking" : "ready", + managed: true, + desired: true, + pid: child.pid, + terminalMode: "external", + command: displayCommand, + cwd: resolved.cwd, + detail: "\u5916\u90e8 Windows \u7ec8\u7aef\u5df2\u6253\u5f00\uff0c\u6b63\u5728\u68c0\u6d4b\u670d\u52a1\u7aef\u53e3", + startedAt: Date.now(), + }); + + void this.waitUntilReady(definition); + return this.toDescriptor(definition, this.getState(serviceId)); + } + + const session = this.pty.start({ + id: sessionId, + title: definition.name, + cwd: resolved.cwd, + command: useCommandLine ? undefined : resolved.command, + commandLine: useCommandLine ? resolved.commandLine : undefined, + cols: SERVICE_TERMINAL_COLS, + rows: SERVICE_TERMINAL_ROWS, + encoding: "auto", + env: Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined, + }); + + this.setState(serviceId, { + ...this.getState(serviceId), + status: "running", + health: definition.readyPorts.length > 0 ? "checking" : "ready", + managed: true, + desired: true, + pid: session.pid, + terminalMode: "embedded", + ptySessionId: session.id, + command: displayCommand, + cwd: resolved.cwd, + detail: "PTY \u5df2\u542f\u52a8\uff0c\u6b63\u5728\u68c0\u6d4b\u670d\u52a1\u7aef\u53e3", + startedAt: Date.now(), + }); + + void this.waitUntilReady(definition); + return this.toDescriptor(definition, this.getState(serviceId)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const current = this.getState(serviceId); + if (!current.desired && !current.ptySessionId) { + this.logs.append(serviceId, "system", `start cancelled before PTY session was created: ${message}`); + this.setState(serviceId, { + ...current, + status: "stopped", + health: "unknown", + managed: false, + pid: undefined, + error: undefined, + detail: "\u5df2\u53d6\u6d88\u542f\u52a8", + stoppedAt: Date.now(), + }); + return this.toDescriptor(definition, this.getState(serviceId)); + } + this.logs.append(serviceId, "system", `start process failed: ${message}`); + this.setState(serviceId, { + ...this.getState(serviceId), + status: "error", + health: "unreachable", + managed: false, + desired: false, + error: message, + detail: message, + pid: undefined, + stoppedAt: Date.now(), + }); + throw error; + } + } + + async stop(serviceId: ServiceId): Promise { + const definition = this.getDefinition(serviceId); + const state = this.getState(serviceId); + if (!state.ptySessionId && state.status === "starting") { + const cancelledDependencyUpdate = + serviceId === "maibot" ? (this.pythonDependencyManager?.cancelStartupUpgrade() ?? false) : false; + this.logs.append(serviceId, "system", "startup cancelled before PTY session was created"); + this.setState(serviceId, { + ...state, + status: "stopped", + health: "unknown", + desired: false, + managed: false, + detail: cancelledDependencyUpdate ? "已取消启动并中断依赖更新" : "已取消启动", + stoppedAt: Date.now(), + }); + return this.toDescriptor(definition, this.getState(serviceId)); + } + if (state.terminalMode === "external" && state.pid) { + return this.stopExternalTerminal(definition, state, false); + } + if (!state.ptySessionId || state.status === "stopped") { + return this.toDescriptor(definition, state); + } + + this.setState(serviceId, { + ...state, + status: "stopping", + desired: false, + detail: "正在温和停止后台 PTY,超时后会强制结束", + }); + this.logs.append(serviceId, "system", "stop requested"); + this.clearRestartTimer(state); + + try { + this.pty.stop({ sessionId: state.ptySessionId, forceAfterMs: STOP_FORCE_AFTER_MS }); + } catch (error) { + this.logs.append(serviceId, "system", `soft stop failed: ${String(error)}`); + this.setState(serviceId, { + ...this.getState(serviceId), + status: "stopped", + health: "unknown", + managed: false, + desired: false, + pid: undefined, + detail: "PTY \u4f1a\u8bdd\u4e0d\u5b58\u5728\uff0c\u5df2\u6807\u8bb0\u4e3a\u505c\u6b62", + stoppedAt: Date.now(), + }); + } + + const nextState = this.getState(serviceId); + this.clearStopTimer(nextState); + nextState.stopTimer = setTimeout(() => { + void this.kill(serviceId); + }, STOP_FORCE_AFTER_MS + 500); + this.states.set(serviceId, nextState); + return this.toDescriptor(definition, nextState); + } + + async kill(serviceId: ServiceId): Promise { + const definition = this.getDefinition(serviceId); + const state = this.getState(serviceId); + if (state.terminalMode === "external" && state.pid) { + return this.stopExternalTerminal(definition, state, true); + } + if (!state.ptySessionId) { + if (state.status === "starting") { + const cancelledDependencyUpdate = + serviceId === "maibot" ? (this.pythonDependencyManager?.cancelStartupUpgrade() ?? false) : false; + this.logs.append(serviceId, "system", "startup force-cancelled before PTY session was created"); + this.setState(serviceId, { + ...state, + status: "stopped", + health: "unknown", + desired: false, + managed: false, + pid: undefined, + error: undefined, + detail: cancelledDependencyUpdate ? "\u5df2\u5f3a\u5236\u53d6\u6d88\u542f\u52a8\u5e76\u4e2d\u65ad\u4f9d\u8d56\u66f4\u65b0" : "\u5df2\u5f3a\u5236\u53d6\u6d88\u542f\u52a8", + stoppedAt: Date.now(), + }); + return this.toDescriptor(definition, this.getState(serviceId)); + } + return this.toDescriptor(definition, state); + } + + this.logs.append(serviceId, "system", "force kill requested"); + this.clearRestartTimer(state); + this.setState(serviceId, { + ...state, + desired: false, + detail: "\u6b63\u5728\u5f3a\u5236\u7ed3\u675f\u540e\u53f0 PTY \u8fdb\u7a0b\u6811", + }); + + try { + this.pty.kill(state.ptySessionId); + } catch (error) { + this.logs.append(serviceId, "system", `force kill failed: ${String(error)}`); + this.setState(serviceId, { + ...this.getState(serviceId), + status: "stopped", + health: "unknown", + managed: false, + pid: undefined, + detail: "PTY \u4f1a\u8bdd\u4e0d\u5b58\u5728\uff0c\u5df2\u6807\u8bb0\u4e3a\u505c\u6b62", + stoppedAt: Date.now(), + }); + } + + return this.toDescriptor(definition, this.getState(serviceId)); + } + + async refresh(): Promise { + this.attachLivePtySessions(); + this.reconcileExitedPtySessions(); + + for (const definition of this.definitions) { + const state = this.getState(definition.id); + const dynamicUrl = await this.resolveServiceUrl(definition.id, definition.url); + if (state.managed && state.status === "running") { + const ready = await this.areReadyPortsOpen(definition); + const healthFailures = ready ? 0 : (state.healthFailures ?? 0) + 1; + this.setState(definition.id, { + ...state, + health: ready ? "ready" : healthFailures >= 3 ? "unreachable" : "checking", + healthFailures, + dynamicUrl, + detail: ready ? "服务端口可访问" : healthFailures >= 3 ? "服务端口连续不可达" : state.detail, + }); + } else if (!state.managed && definition.readyPorts.length > 0) { + const occupied = await this.areReadyPortsOpen(definition); + if (occupied) { + this.setState(definition.id, { + ...state, + health: "conflict", + dynamicUrl, + detail: "\u9ed8\u8ba4\u7aef\u53e3\u5df2\u88ab\u5916\u90e8\u8fdb\u7a0b\u5360\u7528", + }); + } else if (state.dynamicUrl !== dynamicUrl) { + this.setState(definition.id, { + ...state, + dynamicUrl, + }); + } + } else if (state.dynamicUrl !== dynamicUrl) { + this.setState(definition.id, { + ...state, + dynamicUrl, + }); + } + } + return this.snapshot(); + } + + snapshot(): ServiceDescriptor[] { + return this.definitions.map((definition) => this.toDescriptor(definition, this.getState(definition.id))); + } + + async getCommandConfigs(): Promise { + return Promise.all(this.definitions.map((definition) => this.getCommandConfig(definition))); + } + + async saveCommandConfig(update: ServiceCommandUpdate): Promise { + this.getDefinition(update.serviceId); + await this.commandStore.set(update.serviceId, { + cwd: update.cwd, + commandLine: update.commandLine, + }); + this.emit("snapshot", this.snapshot()); + return this.getCommandConfigs(); + } + + async resetCommandConfig(serviceId: ServiceId): Promise { + this.getDefinition(serviceId); + await this.commandStore.reset(serviceId); + this.emit("snapshot", this.snapshot()); + return this.getCommandConfigs(); + } + + getRuntimePathConfigs(): RuntimePathConfig[] { + return this.getRuntimePathDefinitions().map((definition) => this.toRuntimePathConfig(definition)); + } + + async saveRuntimePathConfig(update: RuntimePathUpdate): Promise { + this.getRuntimePathDefinition(update.key); + await this.runtimePathStore.set(update.key, update.value); + this.definitions = this.createDefinitions(); + this.emit("snapshot", this.snapshot()); + return this.getRuntimePathConfigs(); + } + + async resetRuntimePathConfig(key: RuntimePathKey): Promise { + this.getRuntimePathDefinition(key); + await this.runtimePathStore.reset(key); + this.definitions = this.createDefinitions(); + this.emit("snapshot", this.snapshot()); + return this.getRuntimePathConfigs(); + } + + getTerminalSettings(): TerminalSettings { + return this.terminalSettingsStore.get(); + } + + async saveTerminalSettings(settings: TerminalSettings): Promise { + const nextSettings = await this.terminalSettingsStore.set(settings); + this.emit("snapshot", this.snapshot()); + return nextSettings; + } + + reloadRuntimePaths(): void { + this.definitions = this.createDefinitions(); + this.emit("snapshot", this.snapshot()); + } + + dispose(): void { + clearInterval(this.watchdogTimer); + for (const serviceId of SERVICE_IDS) { + const state = this.getState(serviceId); + this.clearStopTimer(state); + this.clearRestartTimer(state); + void this.kill(serviceId); + } + this.removeAllListeners(); + } + + private createDefinitions(): ServiceDefinition[] { + const python = this.getRuntimePath("python"); + const maibotRoot = this.paths.maibotRoot; + const napcatRoot = this.paths.napcatRoot; + const qqBackend = this.initManager.getQqBackendSync(); + const snowlumaRoot = this.paths.snowlumaRoot; + const snowlumaNode = join(snowlumaRoot, "node.exe"); + const snowlumaEntry = join(snowlumaRoot, "index.mjs"); + const napcatExe = join(napcatRoot, "NapCatWinBootMain.exe"); + const napcatNode = join(napcatRoot, "node.exe"); + const napcatNodeEntry = join(napcatRoot, "index.js"); + const napcatLauncherName = "napcat-launch.cmd"; + const napcatLauncherPath = join(napcatRoot, napcatLauncherName); + const cmdShell = process.env.ComSpec || "cmd.exe"; + + return [ + { + id: "maibot", + name: "MaiBot Core", + port: 8001, + ports: [8001], + url: "http://127.0.0.1:8001", + cwd: maibotRoot, + defaultRequiredPaths: [python, maibotRoot, join(maibotRoot, "bot.py")], + conflictPorts: [8001], + readyPorts: [8001], + buildDefaultCommand: async () => [python, "bot.py"], + buildDefaultCommandLine: async () => `${quoteCommandPart(python)} bot.py`, + }, + { + id: "napcat", + name: qqBackend === "snowluma" ? "SnowLuma" : "NapCat", + port: qqBackend === "snowluma" ? 5099 : 6099, + ports: qqBackend === "snowluma" ? [5099, 7988] : [6099], + url: qqBackend === "snowluma" ? "http://127.0.0.1:5099" : "http://127.0.0.1:6099/webui", + cwd: qqBackend === "snowluma" ? snowlumaRoot : napcatRoot, + defaultRequiredPaths: qqBackend === "snowluma" ? [snowlumaRoot, snowlumaEntry] : [napcatRoot], + conflictPorts: qqBackend === "snowluma" ? [5099, 7988] : [6099], + readyPorts: qqBackend === "snowluma" ? [5099] : [6099], + displayDefaultCommandLine: async () => { + if (qqBackend === "snowluma") { + return existsSync(snowlumaNode) + ? `${quoteCommandPart(snowlumaNode)} index.mjs` + : "node index.mjs"; + } + if (existsSync(napcatNode) && existsSync(napcatNodeEntry)) { + return `${quoteCommandPart(napcatNode)} index.js -q `; + } + return `${quoteCommandPart(napcatExe)} -q `; + }, + buildDefaultCommand: async () => { + if (qqBackend === "snowluma") { + return existsSync(snowlumaNode) ? [snowlumaNode, snowlumaEntry] : ["node", snowlumaEntry]; + } + const qq = await this.initManager.readQqAccount(); + await this.initManager.ensureNapCatWebUiConfig(); + if (existsSync(napcatNode) && existsSync(napcatNodeEntry)) { + return qq ? [napcatNode, napcatNodeEntry, "-q", qq] : [napcatNode, napcatNodeEntry]; + } + if (process.platform === "win32" && existsSync(napcatLauncherPath)) { + // 閫氳繃 cmd.exe 璋冪敤纾佺洏涓婄殑 napcat-launch.cmd锛堝凡鍥哄畾 chcp 65001锛夛紝 + // argv 鍚勫厓绱犵嫭绔嬩紶閫掞紝涓嶄細瑙﹀彂 cmd /C 瀛楃涓叉嫾鎺ョ殑寮曞彿姝т箟銆? + const args = ["/D", "/S", "/C", napcatLauncherName]; + if (qq) { + args.push("-q", qq); + } + return [cmdShell, ...args]; + } + return qq ? [napcatExe, "-q", qq] : [napcatExe]; + }, + buildDefaultCommandLine: async () => { + if (qqBackend === "snowluma") { + return existsSync(snowlumaNode) + ? `${quoteCommandPart(snowlumaNode)} index.mjs` + : "node index.mjs"; + } + await this.initManager.ensureNapCatWebUiConfig(); + if (existsSync(napcatNode) && existsSync(napcatNodeEntry)) { + return this.applyServicePlaceholders("napcat", `${quoteCommandPart(napcatNode)} index.js -q `); + } + return this.applyServicePlaceholders("napcat", `${quoteCommandPart(napcatExe)} -q `); + }, + }, + ]; + } + + private shouldUseEmbeddedTerminal(): boolean { + return process.platform !== "win32" || this.terminalSettingsStore.get().useEmbeddedTerminal; + } + + private startExternalTerminal( + definition: ServiceDefinition, + resolved: ResolvedServiceCommand, + env: Record, + ): ChildProcess { + const commandLine = `title MaiBot OneKey - ${definition.name} & chcp 65001 > nul & ${resolved.commandLine}`; + const child = spawn(process.env.ComSpec || "cmd.exe", ["/D", "/S", "/K", commandLine], { + cwd: resolved.cwd, + detached: true, + env: createServiceEnv(Object.keys(env).length > 0 ? env : undefined), + shell: false, + stdio: "ignore", + windowsHide: false, + }); + + this.externalProcesses.set(definition.id, child); + child.once("error", (error) => this.handleExternalTerminalError(definition.id, child.pid, error)); + child.once("exit", (code, signal) => this.handleExternalTerminalExit(definition.id, child.pid, code, signal)); + child.unref(); + + this.logs.append(definition.id, "system", `external terminal launched: pid=${child.pid ?? "unknown"}`); + return child; + } + + private handleExternalTerminalError(serviceId: ServiceId, pid: number | undefined, error: unknown): void { + const current = this.getState(serviceId); + if (current.terminalMode !== "external" || current.pid !== pid) { + return; + } + + const message = error instanceof Error ? error.message : String(error); + this.externalProcesses.delete(serviceId); + this.logs.append(serviceId, "system", `external terminal error: ${message}`); + this.setState(serviceId, { + ...current, + status: "error", + health: "unreachable", + managed: false, + desired: false, + pid: undefined, + error: message, + detail: message, + stoppedAt: Date.now(), + }); + } + + private handleExternalTerminalExit( + serviceId: ServiceId, + pid: number | undefined, + code: number | null, + signal: NodeJS.Signals | null, + ): void { + const current = this.getState(serviceId); + if (current.terminalMode !== "external" || current.pid !== pid) { + return; + } + + this.externalProcesses.delete(serviceId); + const shouldRestart = Boolean(current.desired && current.status !== "stopping"); + const stoppedByRequest = current.status === "stopping" || !current.desired; + this.logs.append(serviceId, "system", `external terminal exit: code=${code ?? "null"} signal=${signal ?? "null"}`); + this.setState(serviceId, { + ...current, + status: stoppedByRequest ? "stopped" : "error", + health: "unknown", + managed: false, + pid: undefined, + terminalMode: undefined, + error: stoppedByRequest ? undefined : shouldRestart ? undefined : `外部终端异常退出: ${code ?? "unknown"}`, + detail: stoppedByRequest + ? "外部终端已停止" + : shouldRestart + ? `外部终端退出,准备自动重启: ${code ?? "unknown"}` + : `外部终端异常退出: ${code ?? "unknown"}`, + stoppedAt: Date.now(), + }); + + if (shouldRestart) { + this.scheduleRestart(serviceId); + } + } + + private async stopExternalTerminal( + definition: ServiceDefinition, + state: ServiceState, + force: boolean, + ): Promise { + if (!state.pid) { + return this.toDescriptor(definition, state); + } + + this.clearRestartTimer(state); + this.setState(definition.id, { + ...state, + status: "stopping", + desired: false, + detail: force ? "正在强制结束外部 Windows 终端进程树" : "正在停止外部 Windows 终端进程树", + }); + this.logs.append(definition.id, "system", force ? "external terminal force kill requested" : "external terminal stop requested"); + + try { + await killWindowsProcessTree(state.pid, force); + } catch (error) { + this.logs.append(definition.id, "system", `external terminal stop failed: ${String(error)}`); + } + + this.externalProcesses.delete(definition.id); + this.setState(definition.id, { + ...this.getState(definition.id), + status: "stopped", + health: "unknown", + managed: false, + desired: false, + pid: undefined, + terminalMode: undefined, + error: undefined, + detail: "外部终端已停止", + stoppedAt: Date.now(), + }); + return this.toDescriptor(definition, this.getState(definition.id)); + } + + private async waitUntilReady(definition: ServiceDefinition): Promise { + if (definition.readyPorts.length === 0) { + return; + } + + const ready = await this.areReadyPortsOpen(definition, 20_000); + const state = this.getState(definition.id); + if (state.status !== "running") { + return; + } + + this.setState(definition.id, { + ...state, + health: ready ? "ready" : "unreachable", + healthFailures: ready ? 0 : (state.healthFailures ?? 0) + 1, + detail: ready ? "服务端口可访问" : "PTY 已启动,但端口暂不可访问", + dynamicUrl: await this.resolveServiceUrl(definition.id, definition.url), + }); + } + + private async areReadyPortsOpen(definition: ServiceDefinition, timeoutMs?: number): Promise { + if (definition.readyPorts.length === 0) { + return true; + } + + const results = await Promise.all( + definition.readyPorts.map((port) => (timeoutMs ? waitForPort(port, timeoutMs) : probePort(port))), + ); + return results.every(Boolean); + } + + private async assertPortsFree(definition: ServiceDefinition): Promise { + for (const port of definition.conflictPorts) { + if (await probePort(port)) { + this.setState(definition.id, { + ...this.getState(definition.id), + health: "conflict", + status: "error", + error: `端口 ${port} 已被占用`, + detail: `端口 ${port} 已被外部进程占用,请停止占用进程后重试`, + }); + throw new Error(`端口 ${port} 已被占用,请停止占用进程后重试`); + } + } + } + + private assertRequiredPaths(definition: ServiceDefinition, paths: string[]): void { + const missing = paths.find((path) => !existsSync(path)); + if (!missing) { + return; + } + + this.setState(definition.id, { + ...this.getState(definition.id), + status: "error", + health: "unreachable", + error: `缺少必要路径: ${missing}`, + detail: `缺少必要路径: ${missing}`, + }); + throw new Error(`缺少必要路径: ${missing}`); + } + + private async resolveStartCommand(definition: ServiceDefinition): Promise { + const override = await this.commandStore.get(definition.id); + const cwd = definition.cwd; + const commandLine = override?.commandLine ? normalizeCommandLine(override.commandLine) : undefined; + if (commandLine) { + const executablePath = extractLeadingExecutablePath(commandLine); + return { + cwd, + commandLine: await this.applyServicePlaceholders(definition.id, commandLine), + requiredPaths: executablePath ? [cwd, executablePath] : [cwd], + customized: true, + }; + } + + const command = definition.buildDefaultCommand ? await definition.buildDefaultCommand() : undefined; + return { + cwd, + command, + commandLine: command ? command.map(quoteCommandPart).join(" ") : await definition.buildDefaultCommandLine(), + requiredPaths: [...definition.defaultRequiredPaths, cwd], + customized: false, + }; + } + + private async getCommandConfig(definition: ServiceDefinition): Promise { + const override = await this.commandStore.get(definition.id); + const defaultCommandLine = definition.displayDefaultCommandLine + ? await definition.displayDefaultCommandLine() + : await definition.buildDefaultCommandLine(); + + return { + serviceId: definition.id, + serviceName: definition.name, + cwd: definition.cwd, + commandLine: override?.commandLine?.trim() || defaultCommandLine, + defaultCwd: definition.cwd, + defaultCommandLine, + customized: Boolean(override?.commandLine?.trim()), + }; + } + + private getRuntimePathDefinitions(): RuntimePathDefinition[] { + return [ + { + key: "python", + label: "Python", + kind: "file", + defaultValue: this.initManager.getPythonPath(), + }, + { + key: "git", + label: "Git", + kind: "file", + defaultValue: this.initManager.getGitPath(), + }, + ]; + } + + private getRuntimePathDefinition(key: RuntimePathKey): RuntimePathDefinition { + const definition = this.getRuntimePathDefinitions().find((item) => item.key === key); + if (!definition) { + throw new Error(`鏈煡璺緞閰嶇疆: ${key}`); + } + return definition; + } + + private getRuntimePath(key: RuntimePathKey): string { + const definition = this.getRuntimePathDefinition(key); + return this.runtimePathStore.get(key) ?? definition.defaultValue; + } + + private isCustomPythonRuntimeEnabled(): boolean { + return Boolean(this.runtimePathStore.get("python")); + } + + private toRuntimePathConfig(definition: RuntimePathDefinition): RuntimePathConfig { + const customValue = this.runtimePathStore.get(definition.key); + return { + key: definition.key, + label: definition.label, + kind: definition.kind, + value: customValue ?? definition.defaultValue, + defaultValue: definition.defaultValue, + customized: Boolean(customValue), + }; + } + + private async applyServicePlaceholders(serviceId: ServiceId, commandLine: string): Promise { + if (serviceId !== "napcat" || !commandLine.includes("")) { + return commandLine; + } + + const qq = await this.initManager.readQqAccount(); + return commandLine + .replace(/(["'])\1/gu, qq ? quoteCommandPart(qq) : "") + .replace(//gu, qq ? quoteCommandPart(qq) : "") + .replace(/\s+/gu, " ") + .trim(); + } + + private async resolveServiceUrl(serviceId: ServiceId, fallback: string): Promise { + if (serviceId === "napcat") { + if (this.initManager.getQqBackendSync() === "snowluma") { + return fallback; + } + return this.resolveNapCatUrl(fallback); + } + if (serviceId === "maibot") { + return this.resolveMaiBotUrl(fallback); + } + return fallback; + } + + private async resolveNapCatUrl(fallback: string): Promise { + try { + const { token } = await this.initManager.readNapCatWebUiToken(); + return token ? `http://127.0.0.1:6099/webui?token=${encodeURIComponent(token)}` : fallback; + } catch { + // 浠讳綍璇诲彇寮傚父閮界洿鎺ュ洖閫€鍒版櫘閫氱櫥褰曢〉锛岄伩鍏嶉樆濉炰富闈㈡澘銆? + return fallback; + } + } + + /** + * MaiBot Core WebUI 鏀寔 `/auth?token=` 鐩存帴鐧诲綍锛? + * webui.json 杩樻湭鐢熸垚鎴栧瓧娈电己澶辨椂鐩存帴鍥為€€涓烘牴鍦板潃锛岀敱鐢ㄦ埛璧版櫘閫氱櫥褰曟祦绋嬨€? + */ + private async resolveMaiBotUrl(fallback: string): Promise { + try { + const { token } = await this.initManager.readMaiBotWebUiToken(); + if (!token) { + return fallback; + } + const base = fallback.replace(/\/+$/u, ""); + return `${base}/auth?token=${encodeURIComponent(token)}`; + } catch { + return fallback; + } + } + + private attachLivePtySessions(): void { + for (const session of this.pty.list()) { + const serviceId = serviceIdFromSession(session.id); + if (!serviceId || !isLivePtyStatus(session.status)) { + continue; + } + + const definition = this.getDefinition(serviceId); + const state = this.getState(serviceId); + if (state.managed && state.ptySessionId === session.id) { + continue; + } + + this.setState(serviceId, { + ...state, + status: "running", + health: definition.readyPorts.length > 0 ? "checking" : "ready", + managed: true, + desired: state.desired ?? true, + pid: session.pid, + terminalMode: "embedded", + ptySessionId: session.id, + command: session.command, + cwd: session.cwd, + detail: `\u5df2\u9644\u52a0\u5230\u540e\u53f0 PTY\uff0cPID ${session.pid ?? "\u672a\u77e5"}`, + startedAt: state.startedAt ?? session.startedAt, + }); + } + } + + private reconcileExitedPtySessions(): void { + const sessions = new Map(this.pty.list().map((session) => [session.id, session])); + + for (const definition of this.definitions) { + const state = this.getState(definition.id); + if (!state.ptySessionId || state.status === "stopped" || state.status === "error") { + continue; + } + + const session = sessions.get(state.ptySessionId); + if (!session || session.status === "starting" || session.status === "running" || session.status === "stopping") { + continue; + } + + const stoppedByRequest = state.status === "stopping" || !state.desired; + this.setState(definition.id, { + ...state, + status: stoppedByRequest ? "stopped" : "error", + health: "unknown", + managed: false, + desired: stoppedByRequest ? false : state.desired, + pid: undefined, + ptySessionId: undefined, + error: stoppedByRequest ? undefined : (session.error ?? `进程异常退出: ${session.exitCode ?? "未知"}`), + detail: stoppedByRequest ? "已停止" : `进程异常退出: ${session.exitCode ?? "未知"}`, + stoppedAt: session.endedAt ?? Date.now(), + }); + } + } + + private handlePtyData(event: PtyDataEvent): void { + const serviceId = serviceIdFromSession(event.sessionId); + if (!serviceId) { + return; + } + + let buffered = `${this.logLineBuffers.get(serviceId) ?? ""}${event.data}`; + buffered = buffered.replace(/\r(?!\n)/gu, "\n"); + const lines = buffered.split(/\n/u); + this.logLineBuffers.set(serviceId, lines.pop() ?? ""); + + for (const line of lines) { + if (line.length > 0) { + this.logs.append(serviceId, "stdout", line); + } + } + } + + private handlePtyExit(event: PtyExitEvent): void { + const serviceId = serviceIdFromSession(event.sessionId); + if (!serviceId) { + return; + } + + const remaining = this.logLineBuffers.get(serviceId); + if (remaining) { + this.logs.append(serviceId, "stdout", remaining); + this.logLineBuffers.delete(serviceId); + } + + const current = this.getState(serviceId); + if (current.ptySessionId !== event.sessionId) { + return; + } + + this.clearStopTimer(current); + const shouldRestart = Boolean(current.desired && current.status !== "stopping"); + const stoppedByRequest = current.status === "stopping" || !current.desired; + this.logs.append(serviceId, "system", `exit: code=${event.exitCode} signal=${event.signal ?? "null"}`); + this.setState(serviceId, { + ...current, + status: stoppedByRequest ? "stopped" : "error", + health: "unknown", + managed: false, + pid: undefined, + ptySessionId: undefined, + detail: stoppedByRequest + ? "已停止" + : shouldRestart + ? `进程退出,准备自动重启: ${event.exitCode}` + : `进程异常退出: ${event.exitCode}`, + error: stoppedByRequest ? undefined : shouldRestart ? undefined : `杩涚▼寮傚父閫€鍑? ${event.exitCode}`, + stoppedAt: Date.now(), + }); + + if (shouldRestart) { + this.scheduleRestart(serviceId); + } + } + + private handlePtyError(event: PtyErrorEvent): void { + const serviceId = serviceIdFromSession(event.sessionId); + if (!serviceId) { + return; + } + + this.logs.append(serviceId, "system", `pty error: ${event.message}`); + this.setState(serviceId, { + ...this.getState(serviceId), + status: "error", + health: "unreachable", + managed: false, + desired: false, + pid: undefined, + error: event.message, + detail: event.message, + stoppedAt: Date.now(), + }); + } + + private handlePtySnapshot(snapshot: PtySessionSnapshot): void { + const serviceId = serviceIdFromSession(snapshot.id); + if (!serviceId) { + return; + } + + const state = this.getState(serviceId); + if (state.ptySessionId !== snapshot.id) { + return; + } + + if (snapshot.status === "exited" || snapshot.status === "error") { + const stoppedByRequest = state.status === "stopping" || !state.desired; + this.setState(serviceId, { + ...state, + status: stoppedByRequest ? "stopped" : "error", + health: "unknown", + managed: false, + desired: stoppedByRequest ? false : state.desired, + pid: undefined, + ptySessionId: undefined, + error: stoppedByRequest ? undefined : (snapshot.error ?? `进程异常退出: ${snapshot.exitCode ?? "未知"}`), + detail: stoppedByRequest ? "已停止" : `进程异常退出: ${snapshot.exitCode ?? "未知"}`, + stoppedAt: snapshot.endedAt ?? Date.now(), + }); + return; + } + + this.setState(serviceId, { + ...state, + pid: snapshot.pid, + command: snapshot.command, + managed: isLivePtyStatus(snapshot.status), + status: snapshot.status === "starting" ? "starting" : snapshot.status === "running" ? "running" : state.status, + }); + } + + private setState(serviceId: ServiceId, state: ServiceState): void { + this.clearStopTimer(this.getState(serviceId)); + this.states.set(serviceId, state); + this.emit("snapshot", this.snapshot()); + } + + private clearStopTimer(state: ServiceState): void { + if (!state.stopTimer) { + return; + } + + clearTimeout(state.stopTimer); + state.stopTimer = undefined; + } + + private clearRestartTimer(state: ServiceState): void { + if (!state.restartTimer) { + return; + } + + clearTimeout(state.restartTimer); + state.restartTimer = undefined; + } + + private scheduleRestart(serviceId: ServiceId): void { + const definition = this.getDefinition(serviceId); + const state = this.getState(serviceId); + const restartAttempts = (state.restartAttempts ?? 0) + 1; + + if (restartAttempts > MAX_RESTART_ATTEMPTS) { + this.setState(serviceId, { + ...state, + desired: false, + restartAttempts, + status: "error", + health: "unreachable", + error: `自动重启超过 ${MAX_RESTART_ATTEMPTS} 次,已停止守护`, + detail: `自动重启超过 ${MAX_RESTART_ATTEMPTS} 次,已停止守护`, + }); + return; + } + + this.clearRestartTimer(state); + const restartTimer = setTimeout(() => { + void this.start(serviceId, false).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + this.logs.append(serviceId, "system", `restart failed: ${message}`); + this.setState(serviceId, { + ...this.getState(serviceId), + status: "error", + health: "unreachable", + error: message, + detail: message, + }); + }); + }, RESTART_DELAY_MS); + + this.setState(serviceId, { + ...state, + desired: true, + restartAttempts, + restartTimer, + status: "stopped", + health: "checking", + managed: false, + pid: undefined, + detail: `${definition.name} \u5f02\u5e38\u9000\u51fa\uff0c${Math.round(RESTART_DELAY_MS / 1000)} \u79d2\u540e\u81ea\u52a8\u91cd\u542f (${restartAttempts}/${MAX_RESTART_ATTEMPTS})`, + }); + } + + private getDefinition(serviceId: ServiceId): ServiceDefinition { + const definition = this.definitions.find((item) => item.id === serviceId); + if (!definition) { + throw new Error(`鏈煡鏈嶅姟: ${serviceId}`); + } + return definition; + } + + private getState(serviceId: ServiceId): ServiceState { + const state = this.states.get(serviceId); + if (!state) { + throw new Error(`鏈煡鏈嶅姟鐘舵€? ${serviceId}`); + } + return state; + } + + private toDescriptor(definition: ServiceDefinition, state: ServiceState): ServiceDescriptor { + return { + id: definition.id, + name: definition.name, + port: definition.port, + ports: definition.ports, + url: state.dynamicUrl ?? definition.url, + status: state.status, + health: state.health, + managed: state.managed, + desired: state.desired, + restartAttempts: state.restartAttempts, + pid: state.pid, + terminalMode: state.terminalMode, + detail: state.detail, + cwd: state.cwd ?? definition.cwd, + command: state.command, + logPath: this.logs.getServiceLogPath(definition.id), + startedAt: state.startedAt, + stoppedAt: state.stoppedAt, + error: state.error, + }; + } +} diff --git a/src/main/services/service-registry.ts b/src/main/services/service-registry.ts new file mode 100644 index 0000000..fc083fd --- /dev/null +++ b/src/main/services/service-registry.ts @@ -0,0 +1,32 @@ +import type { ServiceDescriptor } from "../../shared/contracts"; + +const defaultServices: ServiceDescriptor[] = [ + { + id: "maibot", + name: "MaiBot Core", + port: 8001, + ports: [8001], + url: "http://127.0.0.1:8001", + status: "stopped", + health: "unknown", + managed: false, + desired: false, + detail: "等待初始化向导接入启动流程", + }, + { + id: "napcat", + name: "NapCat", + port: 6099, + ports: [6099], + url: "http://127.0.0.1:6099/webui", + status: "stopped", + health: "unknown", + managed: false, + desired: false, + detail: "仅负责启动并提供 WebUI 快捷入口", + }, +]; + +export function createServiceSnapshot(): ServiceDescriptor[] { + return defaultServices.map((service) => ({ ...service })); +} diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..bf72726 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,253 @@ +import { contextBridge, ipcRenderer } from "electron"; +import type { + CloseAction, + DesktopBridge, + DesktopSnapshot, + InitRepairResult, + InitState, + LogEntry, + LocalChatConnectionState, + LocalChatConnectRequest, + LocalChatEvent, + LocalChatMessageEvent, + LocalChatSendRequest, + LauncherResetResult, + MaiBotConfigFileName, + MaiBotConfigImportResult, + MaiBotDataImportResult, + MaiBotDataResetResult, + MaiBotInstalledPlugin, + MaiBotPluginConfigSaveResult, + MaiBotPluginConfigState, + MaiBotPluginConfigValue, + MaiBotPluginListOptions, + MaiBotPluginListResult, + MaiBotPluginOperationRequest, + MaiBotPluginOperationResult, + MaiBotPluginReadmeResult, + MaiBotPluginStats, + MaiBotStatisticSummary, + ManagedPythonPackageName, + ModuleUpdateResult, + ModuleSourceConfig, + ModuleSourceUpdate, + ModuleTagOption, + PythonOverridesState, + PythonRuntimeCandidate, + PythonPackageSourcePreset, + PythonPackageInstallRequest, + PythonPackageInstallResult, + PythonPackageVersionList, + PtyDataEvent, + PtyErrorEvent, + PtyExitEvent, + PtyInputRequest, + PtyResizeRequest, + PtySessionSnapshot, + PtyStartRequest, + PtyStopRequest, + QqBackend, + QqAccountSetupRequest, + RuntimePathConfig, + RuntimePathKey, + RuntimePathUpdate, + RuntimeResourcePathChangeResult, + RuntimeResourcePathKey, + ServiceCommandConfig, + ServiceCommandUpdate, + ServiceDescriptor, + ServiceId, + SnowLumaResetResult, + StartupAgreementConfirmResult, + StartupAgreementState, + TerminalSettings, + WindowState, +} from "../shared/contracts"; + +function onIpc(channel: string, callback: (event: T) => void): () => void { + const listener = (_event: Electron.IpcRendererEvent, payload: T): void => callback(payload); + ipcRenderer.on(channel, listener); + + return () => { + ipcRenderer.removeListener(channel, listener); + }; +} + +const desktopBridge: DesktopBridge = { + getSnapshot: () => ipcRenderer.invoke("desktop:getSnapshot") as Promise, + openLogsDirectory: () => ipcRenderer.invoke("desktop:openLogsDirectory") as Promise, + openPath: (path: string) => ipcRenderer.invoke("desktop:openPath", path) as Promise, + openExternal: (url: string) => ipcRenderer.invoke("desktop:openExternal", url) as Promise, + chooseCloseAction: (action: CloseAction) => + ipcRenderer.invoke("desktop:chooseCloseAction", action) as Promise, + onCloseRequest: (callback: () => void) => { + const listener = (): void => callback(); + ipcRenderer.on("desktop:close-request", listener); + + return () => { + ipcRenderer.removeListener("desktop:close-request", listener); + }; + }, + onSnapshot: (callback: (snapshot: DesktopSnapshot) => void) => + onIpc("desktop:snapshot", callback), + window: { + minimize: () => ipcRenderer.invoke("desktop:window:minimize") as Promise, + toggleMaximize: () => ipcRenderer.invoke("desktop:window:toggleMaximize") as Promise, + close: () => ipcRenderer.invoke("desktop:window:close") as Promise, + setFloatingMode: (enabled: boolean) => + ipcRenderer.invoke("desktop:window:setFloatingMode", enabled) as Promise, + setFloatingPanelExpanded: (expanded: boolean) => + ipcRenderer.invoke("desktop:window:setFloatingPanelExpanded", expanded) as Promise, + moveFloatingBy: (deltaX: number, deltaY: number) => + ipcRenderer.invoke("desktop:window:moveFloatingBy", deltaX, deltaY) as Promise, + moveFloatingTo: (screenX: number, screenY: number, offsetX: number, offsetY: number) => + ipcRenderer.invoke("desktop:window:moveFloatingTo", screenX, screenY, offsetX, offsetY) as Promise, + finishFloatingDrag: () => + ipcRenderer.invoke("desktop:window:finishFloatingDrag") as Promise, + getState: () => ipcRenderer.invoke("desktop:window:getState") as Promise, + onState: (callback: (state: WindowState) => void) => onIpc("desktop:window-state", callback), + }, + init: { + getState: () => ipcRenderer.invoke("init:getState") as Promise, + repair: () => ipcRenderer.invoke("init:repair") as Promise, + resetSnowLuma: () => + ipcRenderer.invoke("init:resetSnowLuma") as Promise, + setQqBackend: (backend: QqBackend) => + ipcRenderer.invoke("init:setQqBackend", backend) as Promise, + setQqAccount: (request: QqAccountSetupRequest) => + ipcRenderer.invoke("init:setQqAccount", request) as Promise, + }, + agreements: { + getState: () => ipcRenderer.invoke("agreements:getState") as Promise, + confirm: () => ipcRenderer.invoke("agreements:confirm") as Promise, + }, + modules: { + updateMaiBot: (tag?: string) => ipcRenderer.invoke("modules:updateMaibot", tag) as Promise, + listMaiBotTags: () => ipcRenderer.invoke("modules:listMaibotTags") as Promise, + getSourceConfig: () => ipcRenderer.invoke("modules:getSourceConfig") as Promise, + saveSourceConfig: (config: ModuleSourceUpdate) => + ipcRenderer.invoke("modules:saveSourceConfig", config) as Promise, + }, + data: { + importMaiBotDatabase: () => + ipcRenderer.invoke("data:importMaibotDb") as Promise, + importMaiBotConfig: (fileName: MaiBotConfigFileName) => + ipcRenderer.invoke( + "data:importMaibotConfig", + fileName, + ) as Promise, + resetMaiBotData: () => + ipcRenderer.invoke("data:resetMaibotData") as Promise, + }, + launcher: { + resetSettings: () => + ipcRenderer.invoke("launcher:resetSettings") as Promise, + resetAll: () => + ipcRenderer.invoke("launcher:resetAll") as Promise, + }, + plugins: { + listMarket: (serviceUrl?: string, options?: MaiBotPluginListOptions) => + ipcRenderer.invoke("plugins:listMarket", serviceUrl, options) as Promise, + listInstalled: (serviceUrl?: string) => + ipcRenderer.invoke("plugins:listInstalled", serviceUrl) as Promise, + install: (request: MaiBotPluginOperationRequest) => + ipcRenderer.invoke("plugins:install", request) as Promise, + update: (request: MaiBotPluginOperationRequest) => + ipcRenderer.invoke("plugins:update", request) as Promise, + uninstall: (pluginId: string) => + ipcRenderer.invoke("plugins:uninstall", pluginId) as Promise, + getConfig: (pluginId: string, serviceUrl?: string) => + ipcRenderer.invoke("plugins:getConfig", pluginId, serviceUrl) as Promise, + saveConfig: (pluginId: string, config: Record, serviceUrl?: string) => + ipcRenderer.invoke("plugins:saveConfig", pluginId, config, serviceUrl) as Promise, + getReadme: (pluginId: string, repositoryUrl?: string) => + ipcRenderer.invoke("plugins:getReadme", pluginId, repositoryUrl) as Promise, + getStats: (pluginId: string) => + ipcRenderer.invoke("plugins:getStats", pluginId) as Promise, + }, + statistics: { + getMaiBot: () => + ipcRenderer.invoke("statistics:getMaibot") as Promise, + }, + pythonDeps: { + getState: () => ipcRenderer.invoke("pythonDeps:getState") as Promise, + saveSourcePreset: (preset: PythonPackageSourcePreset) => + ipcRenderer.invoke("pythonDeps:saveSourcePreset", preset) as Promise, + listVersions: (packageName: ManagedPythonPackageName) => + ipcRenderer.invoke("pythonDeps:listVersions", packageName) as Promise, + installVersion: (request: PythonPackageInstallRequest) => + ipcRenderer.invoke("pythonDeps:installVersion", request) as Promise, + }, + services: { + start: (serviceId: ServiceId) => + ipcRenderer.invoke("services:start", serviceId) as Promise, + stop: (serviceId: ServiceId) => + ipcRenderer.invoke("services:stop", serviceId) as Promise, + restart: (serviceId: ServiceId) => + ipcRenderer.invoke("services:restart", serviceId) as Promise, + startAll: () => ipcRenderer.invoke("services:startAll") as Promise, + stopAll: () => ipcRenderer.invoke("services:stopAll") as Promise, + refresh: () => ipcRenderer.invoke("services:refresh") as Promise, + saveCommandConfig: (config: ServiceCommandUpdate) => + ipcRenderer.invoke("services:saveCommandConfig", config) as Promise, + resetCommandConfig: (serviceId: ServiceId) => + ipcRenderer.invoke("services:resetCommandConfig", serviceId) as Promise, + saveRuntimePathConfig: (config: RuntimePathUpdate) => + ipcRenderer.invoke("services:saveRuntimePathConfig", config) as Promise, + resetRuntimePathConfig: (key: RuntimePathKey) => + ipcRenderer.invoke("services:resetRuntimePathConfig", key) as Promise, + listPythonRuntimeCandidates: () => + ipcRenderer.invoke("services:listPythonRuntimeCandidates") as Promise, + selectPythonRuntimePath: () => + ipcRenderer.invoke("services:selectPythonRuntimePath") as Promise, + saveTerminalSettings: (settings: TerminalSettings) => + ipcRenderer.invoke("services:saveTerminalSettings", settings) as Promise, + onSnapshot: (callback: (services: ServiceDescriptor[]) => void) => + onIpc("services:snapshot", callback), + }, + resources: { + migratePath: (key: RuntimeResourcePathKey) => + ipcRenderer.invoke("resources:migratePath", key) as Promise, + selectPath: (key: RuntimeResourcePathKey) => + ipcRenderer.invoke("resources:selectPath", key) as Promise, + savePath: (key: RuntimeResourcePathKey, path: string) => + ipcRenderer.invoke("resources:savePath", key, path) as Promise, + resetPath: (key: RuntimeResourcePathKey) => + ipcRenderer.invoke("resources:resetPath", key) as Promise, + }, + logs: { + list: () => ipcRenderer.invoke("logs:list") as Promise, + clear: () => ipcRenderer.invoke("logs:clear") as Promise, + onEntry: (callback: (entry: LogEntry) => void) => onIpc("logs:entry", callback), + }, + localChat: { + connect: (request?: LocalChatConnectRequest) => + ipcRenderer.invoke("localChat:connect", request) as Promise, + disconnect: () => ipcRenderer.invoke("localChat:disconnect") as Promise, + send: (request: LocalChatSendRequest) => + ipcRenderer.invoke("localChat:send", request) as Promise, + listMessages: () => + ipcRenderer.invoke("localChat:listMessages") as Promise, + onEvent: (callback: (event: LocalChatEvent) => void) => onIpc("localChat:event", callback), + }, + pty: { + start: (request: PtyStartRequest) => + ipcRenderer.invoke("pty:start", request) as Promise, + stop: (request: PtyStopRequest) => ipcRenderer.invoke("pty:stop", request) as Promise, + kill: (sessionId: string) => ipcRenderer.invoke("pty:kill", sessionId) as Promise, + input: (request: PtyInputRequest) => ipcRenderer.invoke("pty:input", request) as Promise, + resize: (request: PtyResizeRequest) => + ipcRenderer.invoke("pty:resize", request) as Promise, + clear: (sessionId: string) => ipcRenderer.invoke("pty:clear", sessionId) as Promise, + list: () => ipcRenderer.invoke("pty:list") as Promise, + getBuffer: (sessionId: string) => + ipcRenderer.invoke("pty:getBuffer", sessionId) as Promise, + onData: (callback: (event: PtyDataEvent) => void) => onIpc("pty:data", callback), + onExit: (callback: (event: PtyExitEvent) => void) => onIpc("pty:exit", callback), + onError: (callback: (event: PtyErrorEvent) => void) => onIpc("pty:error", callback), + onSnapshot: (callback: (snapshot: PtySessionSnapshot) => void) => + onIpc("pty:snapshot", callback), + }, +}; + +contextBridge.exposeInMainWorld("maibotDesktop", desktopBridge); diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..7cdda39 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + + + MaiBot OneKey + + +
+ + + diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx new file mode 100644 index 0000000..91153ef --- /dev/null +++ b/src/renderer/src/App.tsx @@ -0,0 +1,12 @@ +import { AppErrorBoundary } from "./components/app/AppErrorBoundary"; +import { CloseChoiceDialog } from "./components/app/CloseChoiceDialog"; +import { DesktopShell } from "./components/app/DesktopShell"; + +export function App(): React.JSX.Element { + return ( + + + + + ); +} diff --git a/src/renderer/src/assets/mai2.png b/src/renderer/src/assets/mai2.png new file mode 100644 index 0000000..0b7d08b Binary files /dev/null and b/src/renderer/src/assets/mai2.png differ diff --git a/src/renderer/src/components/app/AppErrorBoundary.tsx b/src/renderer/src/components/app/AppErrorBoundary.tsx new file mode 100644 index 0000000..cc20be2 --- /dev/null +++ b/src/renderer/src/components/app/AppErrorBoundary.tsx @@ -0,0 +1,107 @@ +import { AlertTriangle, Copy, PowerOff, RotateCcw } from "lucide-react"; +import { Component, type ErrorInfo, type ReactNode } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; + +interface AppErrorBoundaryProps { + children: ReactNode; +} + +interface AppErrorBoundaryState { + error?: Error; + copied?: boolean; +} + +export class AppErrorBoundary extends Component { + state: AppErrorBoundaryState = {}; + + static getDerivedStateFromError(error: Error): AppErrorBoundaryState { + return { error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error("[renderer]", error, errorInfo); + } + + private handleRetry = (): void => { + this.setState({ error: undefined, copied: false }); + }; + + private handleReload = (): void => { + window.location.reload(); + }; + + private handleQuit = (): void => { + void window.maibotDesktop?.window?.close(); + }; + + private handleCopy = async (): Promise => { + const { error } = this.state; + if (!error) { + return; + } + const payload = `${error.message}\n\n${error.stack ?? ""}`.trim(); + try { + await navigator.clipboard.writeText(payload); + this.setState({ copied: true }); + setTimeout(() => this.setState({ copied: false }), 1500); + } catch (copyError) { + console.warn("[renderer] copy failed", copyError); + } + }; + + render(): ReactNode { + const { error, copied } = this.state; + if (!error) { + return this.props.children; + } + + return ( +
+
+
+ + + +
+
+

桌面界面加载失败

+ renderer +
+

+ Renderer 抛出未捕获错误。可以尝试重置错误边界,或重载窗口;至少不会再变成白屏。 +

+
+
+ +
+
+ {error.message} +
+
+              {error.stack ?? error.message}
+            
+
+ +
+ + + + +
+
+
+ ); + } +} diff --git a/src/renderer/src/components/app/CloseChoiceDialog.tsx b/src/renderer/src/components/app/CloseChoiceDialog.tsx new file mode 100644 index 0000000..66e116d --- /dev/null +++ b/src/renderer/src/components/app/CloseChoiceDialog.tsx @@ -0,0 +1,85 @@ +import { PowerOff } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, +} from "@/components/ui/dialog"; +import { Kbd } from "@/components/ui/kbd"; +import { getClosePreference, setClosePreference } from "@/lib/close-preference"; +import { useShortcut } from "@/lib/use-shortcut"; + +export function CloseChoiceDialog(): React.JSX.Element | null { + const [open, setOpen] = useState(false); + const [remember, setRemember] = useState(false); + + useEffect(() => { + return window.maibotDesktop?.onCloseRequest(() => { + const preference = getClosePreference(); + if (preference !== "ask") { + window.maibotDesktop?.chooseCloseAction(preference); + return; + } + + setRemember(false); + setOpen(true); + }); + }, []); + + const cancel = useCallback(() => setOpen(false), []); + const minimize = useCallback(() => { + setOpen(false); + if (remember) { + setClosePreference("minimize"); + } + window.maibotDesktop?.chooseCloseAction("minimize"); + }, [remember]); + const quit = useCallback(() => { + setOpen(false); + if (remember) { + setClosePreference("quit"); + } + window.maibotDesktop?.chooseCloseAction("quit"); + }, [remember]); + + useShortcut("Escape", cancel, { enabled: open, allowInEditable: true }); + useShortcut("Enter", minimize, { enabled: open, allowInEditable: true }); + useShortcut("Mod+Q", quit, { enabled: open }); + + return ( + { if (!next) cancel(); }}> + + } + title={"\u5173\u95ed\u6216\u6700\u5c0f\u5316\u5230\u6258\u76d8\uff1f"} + tone="danger" + /> + + + + + + + + + ); +} diff --git a/src/renderer/src/components/app/DesktopShell.tsx b/src/renderer/src/components/app/DesktopShell.tsx new file mode 100644 index 0000000..0331682 --- /dev/null +++ b/src/renderer/src/components/app/DesktopShell.tsx @@ -0,0 +1,848 @@ +import { + FolderOpen, + GripHorizontal, + Home, + Loader2, + MessageSquare, + Play, + Puzzle, + Radar, + RefreshCw, + Settings, + Square, + TerminalSquare, +} from "lucide-react"; +import { type PointerEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { + DesktopSnapshot, + ServiceDescriptor, + ServiceId, + ServiceStatus, +} from "@shared/contracts"; +import { getDesktopSnapshot, normalizeDesktopSnapshot } from "@/lib/desktop-api"; +import { useShortcut } from "@/lib/use-shortcut"; +import { useTheme } from "@/lib/use-theme"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Kbd } from "@/components/ui/kbd"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Toaster } from "@/components/ui/sonner"; +import maiMascotImage from "@/assets/mai2.png"; +import { HomePanel } from "./HomePanel"; +import { InitializationWizard } from "./InitializationWizard"; +import { LocalChatPanel } from "./LocalChatPanel"; +import { PluginMarketPanel } from "./PluginMarketPanel"; +import { SettingsStatusPanel } from "./SettingsStatusPanel"; +import { StartupAgreementDialog } from "./StartupAgreementDialog"; +import { TerminalPanel } from "./TerminalPanel"; +import { Titlebar } from "./Titlebar"; +import { WebviewPanel } from "./WebviewPanel"; + +const statusText: Record = { + stopped: "未启动", + starting: "启动中", + running: "运行中", + stopping: "停止中", + error: "异常", +}; + +const statusDotColor: Record = { + stopped: "bg-muted-foreground/40", + starting: "bg-warning", + running: "bg-success", + stopping: "bg-warning", + error: "bg-destructive", +}; + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function ServiceChip({ + service, + busy, + onStart, + onStop, + onRestart, +}: { + service: ServiceDescriptor; + busy: boolean; + onStart: (id: ServiceId) => void; + onStop: (id: ServiceId) => void; + onRestart: (id: ServiceId) => void; +}): React.JSX.Element { + const isTransitioning = + service.status === "starting" || service.status === "stopping" || busy; + const isStarting = service.status === "starting"; + const canStart = service.status === "stopped" || service.status === "error"; + const canStop = + service.status === "running" || + service.status === "starting" || + service.status === "error"; + const stopDisabled = !canStop || (busy && !isStarting) || service.status === "stopping"; + + return ( +
+ + {service.name} + + {statusText[service.status]} + +
+ + + + + 启动 + + + + + + 停止 + + + + + + 重启 + +
+
+ ); +} + +function FloatingShell({ + expanded, + edge, + maibotService, + onExpand, + onCollapse, + onRestore, +}: { + expanded: boolean; + edge: "left" | "right" | null; + maibotService: ServiceDescriptor | undefined; + onExpand: () => void; + onCollapse: () => void; + onRestore: () => void; +}): React.JSX.Element { + const dragRef = useRef<{ + offsetX: number; + offsetY: number; + startScreenX: number; + startScreenY: number; + moved: boolean; + pointerId: number; + } | null>(null); + + const updateFloatingState = useCallback(() => undefined, []); + + const startDrag = useCallback((event: PointerEvent) => { + if (!event.isPrimary || (event.pointerType === "mouse" && event.button !== 0)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + if (!event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.setPointerCapture(event.pointerId); + } + dragRef.current = { + offsetX: event.clientX, + offsetY: event.clientY, + startScreenX: event.screenX, + startScreenY: event.screenY, + moved: false, + pointerId: event.pointerId, + }; + }, []); + + const cancelDrag = useCallback((event: PointerEvent) => { + const current = dragRef.current; + if (!current || current.pointerId !== event.pointerId) { + return; + } + dragRef.current = null; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + }, []); + + const drag = useCallback((event: PointerEvent) => { + const current = dragRef.current; + if (!current || current.pointerId !== event.pointerId) { + return; + } + if (event.pointerType === "mouse" && (event.buttons & 1) !== 1) { + cancelDrag(event); + return; + } + const movedDistance = Math.hypot(event.screenX - current.startScreenX, event.screenY - current.startScreenY); + if (movedDistance < 4) { + return; + } + current.moved = true; + void window.maibotDesktop?.window + .moveFloatingTo(event.screenX, event.screenY, current.offsetX, current.offsetY) + .then(updateFloatingState); + }, [cancelDrag, updateFloatingState]); + + const finishDrag = useCallback((event: PointerEvent, clickAction?: () => void) => { + const current = dragRef.current; + if (!current || current.pointerId !== event.pointerId) { + return; + } + dragRef.current = null; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + if (current.moved) { + void window.maibotDesktop?.window.finishFloatingDrag().then(updateFloatingState); + return; + } + clickAction?.(); + }, [updateFloatingState]); + + if (!expanded) { + if (edge) { + return ( +
finishDrag(event)} + onPointerDown={startDrag} + onPointerMove={drag} + onPointerUp={(event) => finishDrag(event, onExpand)} + title="拖动悬浮条,点击展开" + > +
+ +
+
+ ); + } + + return ( +
+ +
+ ); + } + + return ( +
+
finishDrag(event)} + onPointerDown={startDrag} + onPointerMove={drag} + onPointerUp={(event) => finishDrag(event)} + > + + MaiBot 悬浮球 +
event.stopPropagation()} + > + + +
+
+
+ +
+
+ ); +} + +export function DesktopShell(): React.JSX.Element { + const [snapshot, setSnapshot] = useState(null); + const [activeTab, setActiveTab] = useState("home"); + const [pluginMode, setPluginMode] = useState<"market" | "manage">("manage"); + const [requestedConfigPluginId, setRequestedConfigPluginId] = useState(null); + const [actionBusy, setActionBusy] = useState(null); + const [actionError, setActionError] = useState(null); + const [floatingMode, setFloatingMode] = useState(false); + const [floatingExpanded, setFloatingExpanded] = useState(false); + const [floatingEdge, setFloatingEdge] = useState<"left" | "right" | null>(null); + const theme = useTheme(); + + const refreshSnapshot = useCallback(async () => { + const next = await getDesktopSnapshot(); + setSnapshot(next); + return next; + }, []); + + useEffect(() => { + let mounted = true; + refreshSnapshot().then((next) => { + if (mounted) setSnapshot(next); + }); + + const offSnapshot = window.maibotDesktop?.onSnapshot((next) => { + setSnapshot(normalizeDesktopSnapshot(next)); + }); + const offServices = window.maibotDesktop?.services.onSnapshot((services) => { + setSnapshot((current) => (current ? { ...current, services } : current)); + }); + const offLogs = window.maibotDesktop?.logs.onEntry((entry) => { + setSnapshot((current) => + current + ? { + ...current, + recentLogs: [...(current.recentLogs ?? []), entry].slice(-1000), + } + : current, + ); + }); + window.maibotDesktop?.window.getState().then((state) => { + if (mounted) { + setFloatingMode(state.isFloating === true); + setFloatingEdge(state.floatingEdge ?? null); + } + }); + const offWindowState = window.maibotDesktop?.window.onState((state) => { + if (typeof state.isFloating === "boolean") { + setFloatingMode(state.isFloating); + } + setFloatingEdge(state.floatingEdge ?? null); + }); + + return () => { + mounted = false; + offSnapshot?.(); + offServices?.(); + offLogs?.(); + offWindowState?.(); + }; + }, [refreshSnapshot]); + + const services = snapshot?.services ?? []; + const messagePlatformReady = + snapshot?.initState.messagePlatformConfigured === true && + Boolean(snapshot.initState.qqAccount?.trim()); + const visibleServiceChips = services.filter( + (service) => service.id === "maibot" || messagePlatformReady, + ); + const serviceById = useMemo( + () => new Map(services.map((s) => [s.id, s])), + [services], + ); + const maibotService = serviceById.get("maibot"); + const showTerminalTab = snapshot?.terminalSettings.useEmbeddedTerminal === true; + const canInterruptStartup = + actionBusy === "all:start" || + services.some((service) => service.status === "starting"); + + const openLogs = useCallback(() => { + void window.maibotDesktop?.openLogsDirectory(); + }, []); + + const runServiceAction = useCallback( + async ( + key: string, + action: () => Promise, + ) => { + setActionBusy(key); + setActionError(null); + try { + const result = await action(); + const next = Array.isArray(result) ? result : [result]; + setSnapshot((current) => { + if (!current) return current; + const byId = new Map(current.services.map((s) => [s.id, s])); + for (const s of next) byId.set(s.id, s); + return { + ...current, + services: current.services.map((s) => byId.get(s.id) ?? s), + }; + }); + await refreshSnapshot(); + } catch (error) { + setActionError(errorMessage(error)); + } finally { + setActionBusy(null); + } + }, + [refreshSnapshot], + ); + + const startAll = useCallback(() => { + void runServiceAction( + "all:start", + async () => window.maibotDesktop?.services.startAll() ?? [], + ); + }, [runServiceAction]); + const stopAll = useCallback(() => { + void runServiceAction( + "all:stop", + async () => window.maibotDesktop?.services.stopAll() ?? [], + ); + }, [runServiceAction]); + const refreshServices = useCallback(() => { + void runServiceAction( + "all:refresh", + async () => window.maibotDesktop?.services.refresh() ?? [], + ); + }, [runServiceAction]); + const startService = useCallback( + (id: ServiceId) => + void runServiceAction(`${id}:start`, async () => { + if (!window.maibotDesktop) throw new Error("Electron bridge 未连接"); + return window.maibotDesktop.services.start(id); + }), + [runServiceAction], + ); + const stopService = useCallback( + (id: ServiceId) => + void runServiceAction(`${id}:stop`, async () => { + if (!window.maibotDesktop) throw new Error("Electron bridge 未连接"); + return window.maibotDesktop.services.stop(id); + }), + [runServiceAction], + ); + const restartService = useCallback( + (id: ServiceId) => + void runServiceAction(`${id}:restart`, async () => { + if (!window.maibotDesktop) throw new Error("Electron bridge 未连接"); + return window.maibotDesktop.services.restart(id); + }), + [runServiceAction], + ); + + const enterFloatingMode = useCallback(() => { + setFloatingExpanded(false); + setFloatingEdge(null); + void window.maibotDesktop?.window.setFloatingMode(true).then((state) => { + setFloatingMode(state.isFloating === true); + setFloatingEdge(state.floatingEdge ?? null); + }); + }, []); + + const setFloatingPanel = useCallback((expanded: boolean) => { + setFloatingExpanded(expanded); + if (expanded) { + setFloatingEdge(null); + } + void window.maibotDesktop?.window.setFloatingPanelExpanded(expanded).then((state) => { + setFloatingEdge(state.floatingEdge ?? null); + }); + }, []); + + const restoreMainWindow = useCallback(() => { + setFloatingExpanded(false); + setFloatingEdge(null); + void window.maibotDesktop?.window.setFloatingMode(false).then((state) => { + setFloatingMode(state.isFloating === true); + setFloatingEdge(state.floatingEdge ?? null); + }); + }, []); + + const selectTab = useCallback((value: string) => { + if (value === "terminal" && !showTerminalTab) { + setActiveTab("home"); + return; + } + if (value === "pluginmarket") { + setPluginMode("market"); + setActiveTab("plugins"); + return; + } + if (value === "pluginmanage") { + setPluginMode("manage"); + setActiveTab("plugins"); + return; + } + setActiveTab(value); + }, [showTerminalTab]); + + const openPluginConfig = useCallback((pluginId: string) => { + setPluginMode("manage"); + setRequestedConfigPluginId(pluginId); + setActiveTab("plugins"); + }, []); + + useEffect(() => { + if (activeTab === "terminal" && !showTerminalTab) { + setActiveTab("home"); + } + }, [activeTab, showTerminalTab]); + + // Shortcuts + useShortcut("Mod+1", () => selectTab("home")); + useShortcut("Mod+2", () => selectTab("maibot")); + useShortcut("Mod+3", () => selectTab("localchat")); + useShortcut("Mod+4", () => selectTab("terminal"), { enabled: showTerminalTab }); + useShortcut("Mod+5", () => selectTab("pluginmarket")); + useShortcut("Mod+6", () => selectTab("pluginmanage")); + useShortcut("Mod+8", () => selectTab("settings")); + useShortcut("Mod+L", openLogs); + useShortcut("Mod+Shift+S", startAll); + useShortcut("Mod+Shift+X", stopAll); + useShortcut("Mod+Shift+L", theme.toggle); + + if (floatingMode) { + return ( + + setFloatingPanel(false)} + onExpand={() => setFloatingPanel(true)} + onRestore={restoreMainWindow} + /> + + + ); + } + + return ( + +
+ + + {/* Service strip */} +
+
+ {visibleServiceChips.length === 0 ? ( + + 等待服务发现… + + ) : ( + visibleServiceChips.map((service) => ( + + )) + )} +
+
+ + + + + + + 设置 + + + + + + + + + + 启动全部服务 + + + + + + + + + + 停止全部 + + + + + + + + 刷新服务状态 + +
+
+ + {actionError ? ( +
+ {actionError} +
+ ) : null} + + {/* Main */} +
+ +
+ + + + 首页 + + + + + MaiBot + + + + + 随便聊聊 + + + {showTerminalTab ? ( + + + 终端 + + + ) : null} + + + 插件 + + + +
+ + + + + + + 打开日志目录 + + + +
+
+ + + {snapshot ? ( + + ) : ( +
+ + + 正在读取首页状态 + +
+ )} +
+ + + + + + + + + + {showTerminalTab ? ( + + + + ) : null} + + + setRequestedConfigPluginId(null)} + requestedConfigPluginId={requestedConfigPluginId} + /> + + + + {snapshot ? ( + + ) : ( +
+ + + 正在读取桌面状态… + +
+ )} +
+
+
+ + {snapshot ? ( + + ) : null} + {snapshot ? ( + + ) : null} + +
+
+ ); +} diff --git a/src/renderer/src/components/app/HomePanel.tsx b/src/renderer/src/components/app/HomePanel.tsx new file mode 100644 index 0000000..0a82aac --- /dev/null +++ b/src/renderer/src/components/app/HomePanel.tsx @@ -0,0 +1,1773 @@ +import { + ArrowRight, + Download, + ExternalLink, + Loader2, + Maximize2, + MessageSquare, + PackageCheck, + Puzzle, + Radar, + RefreshCw, + Server, + Settings, + Send, + Store, + Wrench, +} from "lucide-react"; +import type React from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import emojiDropImage from "../../../../../emoji2.png"; +import maiDropImage from "../../../../../mai.png"; +import mai2DropImage from "../../../../../mai2.png"; +import maiMascotImage from "@/assets/mai2.png"; +import type { + DesktopSnapshot, + LocalChatEvent, + LocalChatMessageEvent, + MaiBotStatisticSummary, + ModuleSourceConfig, + ModuleSourcePreset, + QqBackend, + ServiceDescriptor, + ServiceStatus, +} from "@shared/contracts"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { localChatErrorMessage } from "@/lib/local-chat-error"; +import { cn } from "@/lib/utils"; +import { WebviewPanel } from "./WebviewPanel"; +import { QuickActionsPanel } from "./QuickActionsPanel"; + +type MaiBotUpdateChannel = "stable" | "test" | "legacy"; +type DashboardUpdateChannel = "stable" | "test"; +type CompactChatState = "idle" | "connecting" | "connected" | "error"; + +const LOCAL_CHAT_USER_NAME_STORAGE_KEY = "maibot.localChat.userName"; +const QQ_WEBUI_PORT_STORAGE_PREFIX = "maibot.qqWebuiPort"; +const ADAPTER_CONFIG_PROMPTED_STORAGE_PREFIX = "maibot.adapterConfigPrompted"; + +export function adapterPluginIdForBackend(backend: QqBackend): string { + return backend === "snowluma" ? "maibot-team.snowluma-adapter" : "maibot-team.napcat-adapter"; +} + +export function markAdapterConfigPrompted(backend: QqBackend): void { + try { + localStorage.setItem(`${ADAPTER_CONFIG_PROMPTED_STORAGE_PREFIX}.${backend}`, "1"); + } catch { + // Local storage may be unavailable in isolated previews. + } +} + +export function shouldPromptAdapterConfig(backend: QqBackend): boolean { + try { + return localStorage.getItem(`${ADAPTER_CONFIG_PROMPTED_STORAGE_PREFIX}.${backend}`) !== "1"; + } catch { + return true; + } +} + +function qqWebuiPortStorageKey(backend: QqBackend): string { + return `${QQ_WEBUI_PORT_STORAGE_PREFIX}.${backend}`; +} + +function defaultQqWebuiPort(backend: QqBackend): string { + return backend === "snowluma" ? "5099" : "6099"; +} + +function readQqWebuiPort(backend: QqBackend): string { + try { + return localStorage.getItem(qqWebuiPortStorageKey(backend)) ?? defaultQqWebuiPort(backend); + } catch { + return defaultQqWebuiPort(backend); + } +} + +function isValidPortText(value: string): boolean { + const port = Number(value); + return Number.isInteger(port) && port >= 1 && port <= 65535; +} + +function qqWebuiUrl(serviceUrl: string | undefined, backend: QqBackend, portText: string): string { + const fallback = backend === "snowluma" ? "http://127.0.0.1:5099" : "http://127.0.0.1:6099/webui"; + const url = new URL(serviceUrl ?? fallback); + if (isValidPortText(portText)) { + url.hostname = "127.0.0.1"; + url.port = String(Number(portText)); + } + if (backend !== "snowluma" && (url.pathname === "/" || url.pathname.length === 0)) { + url.pathname = "/webui"; + } + return url.toString(); +} + +const statusText: Record = { + stopped: "未启动", + starting: "启动中", + running: "运行中", + stopping: "停止中", + error: "异常", +}; + +const statusVariant: Record["variant"]> = { + stopped: "outline", + starting: "warning", + running: "success", + stopping: "warning", + error: "danger", +}; + +function valueOrFallback(value: string | undefined): string { + return value && value.trim().length > 0 ? value : "未读取"; +} + +function formatStatNumber(value: number | undefined): string | undefined { + return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("zh-CN") : undefined; +} + +function messageFromError(error: unknown): string { + return localChatErrorMessage(error); +} + +function localChatText(event: LocalChatMessageEvent): string { + if (event.content.trim()) { + return event.content.trim(); + } + return event.images?.length ? `[图片 ${event.images.length}]` : ""; +} + +function mergeLocalChatMessage( + messages: LocalChatMessageEvent[], + message: LocalChatMessageEvent, +): LocalChatMessageEvent[] { + if (messages.some((item) => item.id === message.id)) { + return messages.map((item) => item.id === message.id ? { ...item, ...message } : item); + } + return [...messages, message].slice(-6); +} + +function DetailRow({ + label, + value, + className, +}: { + label: string; + value: string | undefined; + className?: string; +}): React.JSX.Element { + return ( +
+ {label} + + {valueOrFallback(value)} + +
+ ); +} + +function ChoiceSwitch({ + value, + options, + onChange, +}: { + value: T; + options: Array<{ value: T; label: string; version: string | undefined }>; + onChange: (value: T) => void; +}): React.JSX.Element { + return ( +
+ {options.map((option) => { + const selected = value === option.value; + const disabled = !option.version; + return ( + + ); + })} +
+ ); +} + +function LocalChatQuickCard({ + active, + maibotService, + onOpenFull, +}: { + active: boolean; + maibotService: ServiceDescriptor | undefined; + onOpenFull: () => void; +}): React.JSX.Element { + const [state, setState] = useState("idle"); + const [messages, setMessages] = useState([]); + const [draft, setDraft] = useState(""); + const [error, setError] = useState(null); + const [sending, setSending] = useState(false); + const connected = state === "connected"; + + const connect = useCallback(async () => { + if (!window.maibotDesktop?.localChat) { + setState("error"); + setError("桌面桥未就绪"); + return; + } + if (maibotService?.status !== "running") { + setState("idle"); + setError(null); + return; + } + + setState("connecting"); + setError(null); + try { + const nextState = await window.maibotDesktop.localChat.connect(); + setState(nextState); + const history = await window.maibotDesktop.localChat.listMessages(); + setMessages(history.filter((message) => message.kind !== "planner").slice(-12)); + if (nextState !== "connected") { + setError("MaiBot Core 正在启动或 WebUI 聊天服务还在加载,请稍等片刻后重试。"); + } + } catch (nextError) { + setState("error"); + setError(messageFromError(nextError)); + } + }, [maibotService?.status]); + + useEffect(() => { + if (!active) { + return undefined; + } + + const unsubscribe = window.maibotDesktop?.localChat.onEvent((event: LocalChatEvent) => { + if ("type" in event) { + setState(event.state); + if (event.state === "connected") { + setError(null); + } + return; + } + if (event.kind !== "planner") { + setMessages((current) => mergeLocalChatMessage(current, event)); + } + }); + + void connect(); + return () => { + unsubscribe?.(); + }; + }, [active, connect]); + + const sendQuickMessage = useCallback(async () => { + const content = draft.trim(); + if (!content || !connected || sending || !window.maibotDesktop?.localChat) { + return; + } + + setDraft(""); + setSending(true); + setError(null); + try { + const userName = localStorage.getItem(LOCAL_CHAT_USER_NAME_STORAGE_KEY) ?? "本地用户"; + const sent = await window.maibotDesktop.localChat.send({ content, userName }); + setMessages((current) => mergeLocalChatMessage(current, sent)); + } catch (nextError) { + setDraft(content); + setState("error"); + setError(messageFromError(nextError)); + } finally { + setSending(false); + } + }, [connected, draft, sending]); + + const statusLabel = + connected ? "已连接" : state === "connecting" ? "连接中" : maibotService?.status === "running" ? "未连接" : "MaiBot 未启动"; + const visibleMessages = messages + .map((message) => ({ ...message, text: localChatText(message) })) + .filter((message) => message.text.length > 0) + .slice(-12); + + return ( +
+
+
+ + + +
+

随便聊聊

+
+
+
+ + {statusLabel} + + +
+
+
+ {visibleMessages.length > 0 ? ( + visibleMessages.map((message) => ( +
+

+ {message.text} +

+
+ )) + ) : ( +
+ {error ?? "这里会显示最近几句简单文字。"} +
+ )} +
+
+ setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + void sendQuickMessage(); + } + }} + placeholder={connected ? "输入一句话..." : "启动 MaiBot Core 后可聊天"} + value={draft} + /> + +
+
+ ); +} + +function ServiceSummary({ + icon, + service, + webuiAction, + adapterAction, +}: { + icon: React.ReactNode; + service: ServiceDescriptor | undefined; + webuiAction?: { + label: string; + port: string; + portValid: boolean; + onPortChange: (value: string) => void; + onClick: () => void; + }; + adapterAction?: { + title: string; + description: string; + label: string; + onClick: () => void; + }; +}): React.JSX.Element { + return ( +
+
+
+ + {icon} + +
+

{service?.name ?? "未知服务"}

+
+
+ {service ? ( + + {statusText[service.status]} + + ) : null} +
+ {(webuiAction || adapterAction) ? ( +
+ {webuiAction ? ( +
+ + +
+ ) : null} + {adapterAction ? ( +
+
+

{adapterAction.title}

+

+ {adapterAction.description} +

+
+ +
+ ) : null} +
+ ) : null} +
+ ); +} + +function MessagePlatformConnectCard({ + onClick, +}: { + onClick: () => void; +}): React.JSX.Element { + return ( + + ); +} + +function MaiBotOverviewCard({ + service, + localVersion, + latestStable, + latestPrerelease, + updateBusy, + onUpdate, + onOpenPluginStore, + onOpenPluginManager, +}: { + service: ServiceDescriptor | undefined; + localVersion: string | undefined; + latestStable: string | undefined; + latestPrerelease: string | undefined; + updateBusy?: boolean; + onUpdate: () => void; + onOpenPluginStore: () => void; + onOpenPluginManager: () => void; +}): React.JSX.Element { + const [activeTab, setActiveTab] = useState<"version" | "plugins">("version"); + + return ( +
+
+
+ + + +
+

{service?.name ?? "MaiBot Core"}

+
+
+ {service ? ( + + {statusText[service.status]} + + ) : null} +
+ +
+
+
+ {([ + { value: "version", label: "版本" }, + { value: "plugins", label: "插件" }, + ] as const).map((tab) => ( + + ))} +
+
+ + {activeTab === "version" ? ( +
+
+

MaiBot 本地版本

+

+ {valueOrFallback(localVersion)} +

+
+
+
+ 最新正式版 + + {valueOrFallback(latestStable)} + +
+
+ 最新测试版 + + {valueOrFallback(latestPrerelease)} + +
+ +
+
+ ) : ( +
+
+ + + + + 插件 + + 安装或管理 MaiBot Core 插件。 + + +
+ + + + +
+ )} +
+
+ ); +} + +function HomeStatsPanel({ + snapshot, + services, + onOpenQuickActions, +}: { + snapshot: DesktopSnapshot; + services: ServiceDescriptor[]; + onOpenQuickActions: () => void; +}): React.JSX.Element { + const [maibotStats, setMaibotStats] = useState(null); + const runningCount = services.filter((service) => service.status === "running").length; + const readyCount = services.filter((service) => service.health === "ready").length; + const qqBackend = snapshot.initState.qqBackend === "snowluma" ? "SnowLuma" : "NapCat"; + const topChats = maibotStats?.chatStats.slice(0, 2) ?? []; + + useEffect(() => { + let disposed = false; + + const loadStats = async (): Promise => { + try { + const stats = await window.maibotDesktop?.statistics.getMaiBot(); + if (!disposed) { + setMaibotStats(stats ?? null); + } + } catch { + if (!disposed) { + setMaibotStats(null); + } + } + }; + + void loadStats(); + const timer = window.setInterval(() => { + void loadStats(); + }, 30_000); + + return () => { + disposed = true; + window.clearInterval(timer); + }; + }, [snapshot.paths.maibotRoot]); + + return ( +
+ +
+
+
+ + + +
+

快捷操作

+

路径、数据库和配置导入。

+
+
+ +
+
+
+ ); +} + +interface DroppedMascot { + id: number; + src: string; + collider: ImageAlphaBounds; + targetRect?: CollisionRect; + x: number; + y: number; + vx: number; + vy: number; + size: number; + rotate: number; + vr: number; + lastCollisionAt: number; + bornAt: number; +} + +interface ImageAlphaBounds { + left: number; + top: number; + right: number; + bottom: number; +} + +interface CollisionRect { + left: number; + top: number; + right: number; + bottom: number; +} + +const DROP_MAX_ROTATION_SPEED = 4.2; +const DROP_COLLISION_COOLDOWN_MS = 90; + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +function clampDropRotation(drop: DroppedMascot): void { + drop.vr = clamp(drop.vr, -DROP_MAX_ROTATION_SPEED, DROP_MAX_ROTATION_SPEED); +} + +function randomDropImage(): string { + const roll = Math.random() * 100; + if (roll < 49) return maiDropImage; + if (roll < 98) return mai2DropImage; + return emojiDropImage; +} + +function randomCollisionTarget(): CollisionRect | undefined { + const candidates = Array.from(document.querySelectorAll(".rounded-lg.border")) + .filter((element) => !element.closest("[data-drop-layer='true']")) + .filter((element) => !element.closest("[data-mascot-stage='true']")) + .map((element) => element.getBoundingClientRect()) + .filter((rect) => rect.width > 36 && rect.height > 28 && rect.top < window.innerHeight && rect.bottom > 0); + const rect = candidates[Math.floor(Math.random() * candidates.length)]; + return rect + ? { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom } + : undefined; +} + +function droppedCollisionRect(drop: DroppedMascot): CollisionRect { + return { + left: drop.x + drop.collider.left * drop.size, + top: drop.y + drop.collider.top * drop.size, + right: drop.x + drop.collider.right * drop.size, + bottom: drop.y + drop.collider.bottom * drop.size, + }; +} + +function alphaBoundsForImage(src: string): Promise { + return new Promise((resolve) => { + const image = new Image(); + image.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const context = canvas.getContext("2d", { willReadFrequently: true }); + if (!context || canvas.width === 0 || canvas.height === 0) { + resolve({ left: 0.12, top: 0.12, right: 0.88, bottom: 0.88 }); + return; + } + + context.drawImage(image, 0, 0); + const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data; + let left = canvas.width; + let top = canvas.height; + let right = 0; + let bottom = 0; + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + if (pixels[(y * canvas.width + x) * 4 + 3] <= 18) continue; + left = Math.min(left, x); + top = Math.min(top, y); + right = Math.max(right, x + 1); + bottom = Math.max(bottom, y + 1); + } + } + + if (left >= right || top >= bottom) { + resolve({ left: 0.12, top: 0.12, right: 0.88, bottom: 0.88 }); + return; + } + + resolve({ + left: left / canvas.width, + top: top / canvas.height, + right: right / canvas.width, + bottom: bottom / canvas.height, + }); + }; + image.onerror = () => resolve({ left: 0.12, top: 0.12, right: 0.88, bottom: 0.88 }); + image.src = src; + }); +} + +function ElasticMascot({ onLongPress }: { onLongPress: () => void }): React.JSX.Element { + const stageRef = useRef(null); + const frameRef = useRef(null); + const dropIdRef = useRef(0); + const dropsRef = useRef([]); + const alphaBoundsRef = useRef>({}); + const bodyRef = useRef({ + x: 0, + y: 0, + rotate: 0, + stretch: 0, + squash: 0, + vx: 0, + vy: 0, + vr: 0, + vs: 0, + vq: 0, + }); + const pointerRef = useRef({ x: 0, y: 0, t: 0 }); + const longPressTimerRef = useRef(null); + const longPressTriggeredRef = useRef(false); + const [pose, setPose] = useState({ + x: 0, + y: 0, + rotate: 0, + stretch: 0, + squash: 0, + }); + const [drops, setDrops] = useState([]); + + const kick = useCallback((x: number, y: number, force = 1) => { + const body = bodyRef.current; + body.vx += x * force; + body.vy += y * force; + body.vr += x * 0.18 * force; + body.vs += Math.abs(x) * 0.015 * force + Math.abs(y) * 0.01 * force; + body.vq += y * 0.018 * force; + }, []); + + const spawnDrop = useCallback((clientX?: number) => { + const src = randomDropImage(); + const size = 58 + Math.random() * 34; + const viewportWidth = window.innerWidth || 1024; + const x = Math.max(8, Math.min(viewportWidth - size - 8, (clientX ?? Math.random() * viewportWidth) - size / 2 + (Math.random() - 0.5) * 90)); + const diagonalDirection = Math.random() < 0.5 ? -1 : 1; + const nextDrop: DroppedMascot = { + id: dropIdRef.current++, + src, + collider: alphaBoundsRef.current[src] ?? { left: 0.12, top: 0.12, right: 0.88, bottom: 0.88 }, + targetRect: randomCollisionTarget(), + x, + y: -size - 12, + vx: diagonalDirection * (1.5 + Math.random() * 2.2), + vy: 1 + Math.random() * 2, + size, + rotate: (Math.random() - 0.5) * 32, + vr: (Math.random() - 0.5) * 3.6, + lastCollisionAt: 0, + bornAt: performance.now(), + }; + dropsRef.current = [...dropsRef.current, nextDrop]; + setDrops(dropsRef.current); + }, []); + + useEffect(() => { + let cancelled = false; + void Promise.all([maiDropImage, mai2DropImage, emojiDropImage].map(async (src) => [src, await alphaBoundsForImage(src)] as const)) + .then((entries) => { + if (!cancelled) { + alphaBoundsRef.current = Object.fromEntries(entries); + } + }); + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + const tick = () => { + const now = performance.now(); + const body = bodyRef.current; + body.vx += -body.x * 0.09; + body.vy += -body.y * 0.09; + body.vr += -body.rotate * 0.08; + body.vs += -body.stretch * 0.1; + body.vq += -body.squash * 0.1; + + body.vx *= 0.82; + body.vy *= 0.82; + body.vr *= 0.8; + body.vs *= 0.78; + body.vq *= 0.78; + + body.x += body.vx; + body.y += body.vy; + body.rotate += body.vr; + body.stretch += body.vs; + body.squash += body.vq; + + setPose({ + x: body.x, + y: body.y, + rotate: body.rotate, + stretch: body.stretch, + squash: body.squash, + }); + + const currentDrops = dropsRef.current; + if (currentDrops.length > 0) { + const width = window.innerWidth || 1024; + const height = window.innerHeight || 768; + const nextDrops = currentDrops + .map((drop) => ({ ...drop })) + .filter((drop) => now - drop.bornAt < 10_000 && drop.y < height + drop.size * 2 && drop.x > -drop.size * 2 && drop.x < width + drop.size * 2); + + for (const drop of nextDrops) { + drop.vy += 0.42; + drop.vx *= 0.992; + drop.vy *= 0.995; + drop.vr *= 0.965; + clampDropRotation(drop); + drop.x += drop.vx; + drop.y += drop.vy; + drop.rotate += drop.vr; + + if (drop.x < 0) { + drop.x = 0; + drop.vx = Math.abs(drop.vx) * 0.72; + drop.vr += drop.vx * 0.12; + clampDropRotation(drop); + } else if (drop.x + drop.size > width) { + drop.x = width - drop.size; + drop.vx = -Math.abs(drop.vx) * 0.72; + drop.vr += drop.vx * 0.12; + clampDropRotation(drop); + } + + const rect = drop.targetRect; + if (rect) { + if (now - drop.lastCollisionAt < DROP_COLLISION_COOLDOWN_MS) continue; + const collision = droppedCollisionRect(drop); + const overlaps = + collision.left < rect.right + && collision.right > rect.left + && collision.top < rect.bottom + && collision.bottom > rect.top; + if (!overlaps) continue; + + const fromTop = Math.abs(collision.bottom - rect.top); + const fromBottom = Math.abs(rect.bottom - collision.top); + const fromLeft = Math.abs(collision.right - rect.left); + const fromRight = Math.abs(rect.right - collision.left); + const min = Math.min(fromTop, fromBottom, fromLeft, fromRight); + if (min === fromTop && drop.vy > 0) { + drop.y = rect.top - drop.collider.bottom * drop.size; + drop.vy = -Math.abs(drop.vy) * (0.42 + Math.random() * 0.18); + drop.vx += (Math.random() - 0.5) * 3; + } else if (min === fromBottom && drop.vy < 0) { + drop.y = rect.bottom - drop.collider.top * drop.size; + drop.vy = Math.abs(drop.vy) * 0.35; + } else if (min === fromLeft) { + drop.x = rect.left - drop.collider.right * drop.size; + drop.vx = -Math.abs(drop.vx) * 0.62; + } else { + drop.x = rect.right - drop.collider.left * drop.size; + drop.vx = Math.abs(drop.vx) * 0.62; + } + drop.vr = drop.vr * 0.68 + drop.vx * 0.08; + drop.lastCollisionAt = now; + clampDropRotation(drop); + } + } + + for (let index = 0; index < nextDrops.length; index++) { + for (let otherIndex = index + 1; otherIndex < nextDrops.length; otherIndex++) { + const left = nextDrops[index]; + const right = nextDrops[otherIndex]; + const leftRect = droppedCollisionRect(left); + const rightRect = droppedCollisionRect(right); + if ( + leftRect.left < rightRect.right + && leftRect.right > rightRect.left + && leftRect.top < rightRect.bottom + && leftRect.bottom > rightRect.top + ) { + const leftCenterX = (leftRect.left + leftRect.right) / 2; + const leftCenterY = (leftRect.top + leftRect.bottom) / 2; + const rightCenterX = (rightRect.left + rightRect.right) / 2; + const rightCenterY = (rightRect.top + rightRect.bottom) / 2; + const dx = leftCenterX - rightCenterX || 1; + const dy = leftCenterY - rightCenterY || 1; + const distance = Math.max(1, Math.hypot(dx, dy)); + const push = 0.8; + left.vx += (dx / distance) * push; + left.vy += (dy / distance) * push; + right.vx -= (dx / distance) * push; + right.vy -= (dy / distance) * push; + if (now - left.lastCollisionAt >= DROP_COLLISION_COOLDOWN_MS) { + left.vr = left.vr * 0.72 + left.vx * 0.06; + left.lastCollisionAt = now; + clampDropRotation(left); + } + if (now - right.lastCollisionAt >= DROP_COLLISION_COOLDOWN_MS) { + right.vr = right.vr * 0.72 + right.vx * 0.06; + right.lastCollisionAt = now; + clampDropRotation(right); + } + } + } + } + + dropsRef.current = nextDrops; + setDrops(nextDrops); + } + frameRef.current = window.requestAnimationFrame(tick); + }; + + frameRef.current = window.requestAnimationFrame(tick); + return () => { + if (frameRef.current !== null) { + window.cancelAnimationFrame(frameRef.current); + } + }; + }, []); + + const onPointerMove = useCallback((event: React.PointerEvent) => { + const stage = stageRef.current; + if (!stage) { + return; + } + + const rect = stage.getBoundingClientRect(); + const now = performance.now(); + const previous = pointerRef.current; + const hasPrevious = previous.t > 0; + const localX = event.clientX - rect.left; + const localY = event.clientY - rect.top; + const dx = hasPrevious ? localX - previous.x : 0; + const dy = hasPrevious ? localY - previous.y : 0; + const headBias = 1 - Math.min(1, localY / Math.max(1, rect.height)); + const speed = Math.min(18, Math.hypot(dx, dy)); + + pointerRef.current = { x: localX, y: localY, t: now }; + kick(dx * 0.18 * headBias, dy * 0.16 * headBias - speed * 0.03, 1); + }, [kick]); + + const onPointerEnter = useCallback(() => { + kick(-5, -4, 1.2); + }, [kick]); + + const clearLongPress = useCallback(() => { + if (longPressTimerRef.current !== null) { + window.clearTimeout(longPressTimerRef.current); + longPressTimerRef.current = null; + } + }, []); + + const onPointerLeave = useCallback(() => { + clearLongPress(); + pointerRef.current.t = 0; + kick(4, 2, 0.8); + }, [clearLongPress, kick]); + + const onClick = useCallback((event: React.MouseEvent) => { + if (longPressTriggeredRef.current) { + longPressTriggeredRef.current = false; + return; + } + spawnDrop(event.clientX); + kick((Math.random() - 0.5) * 8, -7, 1.1); + }, [kick, spawnDrop]); + + const onPointerDown = useCallback(() => { + clearLongPress(); + longPressTriggeredRef.current = false; + longPressTimerRef.current = window.setTimeout(() => { + longPressTriggeredRef.current = true; + onLongPress(); + }, 650); + }, [clearLongPress, onLongPress]); + + const onPointerUp = useCallback(() => { + clearLongPress(); + }, [clearLongPress]); + + const stretch = Math.max(-0.1, Math.min(0.16, pose.stretch)); + const squash = Math.max(-0.12, Math.min(0.12, pose.squash)); + const rotate = Math.max(-9, Math.min(9, pose.rotate)); + const x = Math.max(-22, Math.min(22, pose.x)); + const y = Math.max(-18, Math.min(18, pose.y)); + + return ( + + ); +} + +export function HomePanel({ + active, + snapshot, + onSnapshot, + onOpenTab, + onOpenPluginConfig, + onEnterFloatingMode, +}: { + active: boolean; + snapshot: DesktopSnapshot; + onSnapshot: (snapshot: DesktopSnapshot) => void; + onOpenTab: (tab: string) => void; + onOpenPluginConfig: (pluginId: string) => void; + onEnterFloatingMode: () => void; +}): React.JSX.Element { + const [updateDialog, setUpdateDialog] = useState<"maibot" | "dashboard" | null>(null); + const [busy, setBusy] = useState(null); + const [error, setError] = useState(null); + const [messagePlatformDialogOpen, setMessagePlatformDialogOpen] = useState(false); + const [quickActionsOpen, setQuickActionsOpen] = useState(false); + const [messagePlatformBackend, setMessagePlatformBackend] = useState("napcat"); + const [messagePlatformAccount, setMessagePlatformAccount] = useState(snapshot.initState.qqAccount ?? ""); + const [maibotChannel, setMaibotChannel] = useState("stable"); + const [dashboardChannel, setDashboardChannel] = useState("stable"); + const [napcatWebuiOpen, setNapcatWebuiOpen] = useState(false); + const [qqWebuiPort, setQqWebuiPort] = useState(() => readQqWebuiPort(snapshot.initState.qqBackend ?? "napcat")); + const [moduleSourceConfig, setModuleSourceConfig] = useState(null); + const [moduleSourcePreset, setModuleSourcePreset] = useState("ghproxy"); + const [customMaiBotUrl, setCustomMaiBotUrl] = useState(""); + const [customNapcatAdapterUrl, setCustomNapcatAdapterUrl] = useState(""); + const services = snapshot.services ?? []; + const maibot = services.find((service) => service.id === "maibot"); + const napcat = services.find((service) => service.id === "napcat"); + const adapterPluginId = adapterPluginIdForBackend(snapshot.initState.qqBackend ?? "napcat"); + const adapterName = snapshot.initState.qqBackend === "snowluma" ? "SnowLuma 适配器" : "NapCat 适配器"; + const qqWebuiPortValid = isValidPortText(qqWebuiPort); + const qqBackend = snapshot.initState.qqBackend ?? "napcat"; + const currentQqWebuiUrl = qqWebuiUrl(napcat?.url, qqBackend, qqWebuiPort); + const messagePlatformConfigured = + snapshot.initState.messagePlatformConfigured && Boolean(snapshot.initState.qqAccount?.trim()); + const qqBackendBusy = + napcat?.status === "starting" || napcat?.status === "running" || napcat?.status === "stopping" || Boolean(napcat?.managed); + const maibotUpdateBlocked = + maibot?.managed || maibot?.status === "starting" || maibot?.status === "running" || maibot?.status === "stopping"; + + const maibotTargets: Record = { + stable: snapshot.moduleVersions.maibotLatestStableTag, + test: snapshot.moduleVersions.maibotLatestPrereleaseTag, + legacy: snapshot.moduleVersions.maibotLatestLegacyTag, + }; + const dashboardTargets: Record = { + stable: snapshot.moduleVersions.dashboardLatestStablePypi ?? snapshot.moduleVersions.dashboardLatestPypi, + test: snapshot.moduleVersions.dashboardLatestPrereleasePypi, + }; + + const refreshSnapshot = useCallback(async () => { + if (!window.maibotDesktop) { + return; + } + onSnapshot(await window.maibotDesktop.getSnapshot()); + }, [onSnapshot]); + + useEffect(() => { + setQqWebuiPort(readQqWebuiPort(snapshot.initState.qqBackend ?? "napcat")); + }, [snapshot.initState.qqBackend]); + + useEffect(() => { + if (!qqWebuiPortValid) { + return; + } + try { + localStorage.setItem(qqWebuiPortStorageKey(snapshot.initState.qqBackend ?? "napcat"), String(Number(qqWebuiPort))); + } catch { + // Local storage may be unavailable in isolated previews. + } + }, [qqWebuiPort, qqWebuiPortValid, snapshot.initState.qqBackend]); + + const loadModuleSourceConfig = useCallback(async () => { + if (!window.maibotDesktop?.modules) { + return; + } + + const config = await window.maibotDesktop.modules.getSourceConfig(); + setModuleSourceConfig(config); + setModuleSourcePreset(config.preset); + setCustomMaiBotUrl(config.maibotUrl); + setCustomNapcatAdapterUrl(config.napcatAdapterUrl); + }, []); + + const reloadModuleSourceOptions = useCallback(async () => { + if (!window.maibotDesktop?.modules) { + return; + } + + const currentPreset = moduleSourcePreset; + const currentMaiBotUrl = customMaiBotUrl; + const currentNapcatAdapterUrl = customNapcatAdapterUrl; + const config = await window.maibotDesktop.modules.getSourceConfig(); + setModuleSourceConfig(config); + setModuleSourcePreset(currentPreset); + setCustomMaiBotUrl(currentMaiBotUrl); + setCustomNapcatAdapterUrl(currentNapcatAdapterUrl); + }, [customMaiBotUrl, customNapcatAdapterUrl, moduleSourcePreset]); + + const openMaiBotUpdate = useCallback(() => { + setError(null); + setMaibotChannel( + snapshot.moduleVersions.maibotLatestStableTag + ? "stable" + : snapshot.moduleVersions.maibotLatestPrereleaseTag + ? "test" + : "legacy", + ); + setUpdateDialog("maibot"); + void loadModuleSourceConfig().catch((nextError: unknown) => { + setError(messageFromError(nextError)); + }); + }, [ + loadModuleSourceConfig, + snapshot.moduleVersions.maibotLatestPrereleaseTag, + snapshot.moduleVersions.maibotLatestStableTag, + ]); + + const openPluginStore = useCallback(() => { + onOpenTab("pluginmarket"); + }, [onOpenTab]); + + const openPluginManager = useCallback(() => { + onOpenTab("pluginmanage"); + }, [onOpenTab]); + + const openMessagePlatformDialog = useCallback(() => { + setError(null); + setMessagePlatformBackend(snapshot.initState.qqBackend ?? "napcat"); + setMessagePlatformAccount(snapshot.initState.qqAccount ?? ""); + setMessagePlatformDialogOpen(true); + }, [snapshot.initState.qqAccount, snapshot.initState.qqBackend]); + + const setupMessagePlatform = useCallback(async () => { + const qqAccount = messagePlatformAccount.trim(); + if (!/^\d+$/u.test(qqAccount)) { + setError("请输入正确的 QQ 号"); + return; + } + if (!window.maibotDesktop) { + setError("桌面桥未就绪,无法初始化消息平台"); + return; + } + + setBusy("message-platform:setup"); + setError(null); + try { + await window.maibotDesktop.init.setQqAccount({ + qqAccount, + qqBackend: messagePlatformBackend, + }); + await window.maibotDesktop.init.repair(); + await window.maibotDesktop.services.start("napcat"); + toast.success(`${messagePlatformBackend === "snowluma" ? "QQ-SnowLuma" : "QQ-NapCat"} 已配置并启动`); + setMessagePlatformDialogOpen(false); + await refreshSnapshot(); + markAdapterConfigPrompted(messagePlatformBackend); + window.setTimeout(() => onOpenPluginConfig(adapterPluginIdForBackend(messagePlatformBackend)), 250); + } catch (nextError) { + setError(messageFromError(nextError)); + await refreshSnapshot().catch(() => undefined); + } finally { + setBusy(null); + } + }, [messagePlatformAccount, messagePlatformBackend, onOpenPluginConfig, refreshSnapshot]); + + const updateMaiBot = useCallback(async () => { + const target = maibotTargets[maibotChannel]; + if (!window.maibotDesktop?.modules || !target) { + setError("没有可用的目标版本"); + return; + } + + setBusy("maibot:update"); + setError(null); + try { + const config = await window.maibotDesktop.modules.saveSourceConfig({ + preset: moduleSourcePreset, + maibotUrl: customMaiBotUrl, + napcatAdapterUrl: customNapcatAdapterUrl, + }); + setModuleSourceConfig(config); + setModuleSourcePreset(config.preset); + setCustomMaiBotUrl(config.maibotUrl); + setCustomNapcatAdapterUrl(config.napcatAdapterUrl); + await window.maibotDesktop.modules.updateMaiBot(target); + toast.success("MaiBot 更新完成"); + setUpdateDialog(null); + await refreshSnapshot(); + } catch (nextError) { + setError(messageFromError(nextError)); + } finally { + setBusy(null); + } + }, [customMaiBotUrl, customNapcatAdapterUrl, maibotChannel, maibotTargets, moduleSourcePreset, refreshSnapshot]); + + const updateDashboard = useCallback(async () => { + const target = dashboardTargets[dashboardChannel]; + if (!window.maibotDesktop?.pythonDeps || !target) { + setError("没有可用的目标版本"); + return; + } + + setBusy("dashboard:update"); + setError(null); + try { + await window.maibotDesktop.pythonDeps.installVersion({ + packageName: "maibot-dashboard", + version: target, + }); + toast.success("WebUI 更新完成"); + await refreshSnapshot(); + setUpdateDialog(null); + } catch (nextError) { + setError(messageFromError(nextError)); + } finally { + setBusy(null); + } + }, [dashboardChannel, dashboardTargets, refreshSnapshot]); + + return ( + <> +
+
+
+
+ + onOpenTab("localchat")} + /> + {messagePlatformConfigured ? ( + onOpenPluginConfig(adapterPluginId), + }} + icon={} + service={napcat} + webuiAction={{ + label: "打开 WebUI", + port: qqWebuiPort, + portValid: qqWebuiPortValid, + onPortChange: setQqWebuiPort, + onClick: () => setNapcatWebuiOpen(true), + }} + /> + ) : ( + + )} +
+ setQuickActionsOpen(true)} + services={services} + snapshot={snapshot} + /> +
+ +
+
+ + + + } + title="快捷操作" + tone="primary" + /> + + + + + + + + + + { + if (!next && busy !== "message-platform:setup") setMessagePlatformDialogOpen(false); + }} + > + + } + title="新增消息平台" + tone="primary" + /> + + {error && messagePlatformDialogOpen ? ( +
+ {error} +
+ ) : null} +
+ {([ + { + backend: "napcat", + title: "QQ-NapCat", + description: "使用 NapCat WebUI 登录 QQ,并通过 OneBot WebSocket 连接 MaiBot。", + }, + { + backend: "snowluma", + title: "QQ-SnowLuma", + description: "使用 SnowLuma 注入 QQ 进程,并通过 OneBot WebSocket 连接 MaiBot。", + }, + ] as const).map((option) => ( + + ))} +
+ + {qqBackendBusy ? ( +
+ QQ 后端正在运行,请先停止后再新增或切换消息平台。 +
+ ) : null} +
+ + + + +
+
+ + { + if (!next && busy !== "maibot:update") setUpdateDialog(null); + }} + > + + } + title="更新 MaiBot" + tone="primary" + /> + + {error && updateDialog === "maibot" ? ( +
+ {error} +
+ ) : null} +
+ +
+ + + +
+ {maibotUpdateBlocked ? ( +
+ 请先停止 MaiBot Core,再执行更新。 +
+ ) : null} +
+

目标版本

+ +
+
+
+
+

更新源

+

+ 会同步保存到设置中心的模块更新源。 +

+
+ +
+
+ + +
+
+ + + + + + +
+ + { + if (!next && busy !== "dashboard:update") setUpdateDialog(null); + }} + > + + } + title="更新 WebUI" + tone="primary" + /> + + {error && updateDialog === "dashboard" ? ( +
+ {error} +
+ ) : null} +
+ +
+ + +
+ {maibotUpdateBlocked ? ( +
+ 请先停止 MaiBot Core,再更新 WebUI 覆盖依赖。 +
+ ) : null} +
+

目标版本

+ +
+ + + + + + +
+ + + + } + title={`${napcat?.name ?? "QQ 后端"} WebUI`} + tone="primary" + /> + + + + + + + ); +} diff --git a/src/renderer/src/components/app/IdListEditor.tsx b/src/renderer/src/components/app/IdListEditor.tsx new file mode 100644 index 0000000..ddfe1e7 --- /dev/null +++ b/src/renderer/src/components/app/IdListEditor.tsx @@ -0,0 +1,229 @@ +import { Plus, X } from "lucide-react"; +import { useCallback, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; + +export interface IdListEditorProps { + label: string; + values: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + emptyHint?: string; + inputMode?: React.HTMLAttributes["inputMode"]; + disabled?: boolean; + className?: string; + itemAriaLabel?: (value: string, index: number) => string; +} + +const SPLIT_RE = /[\s,,;;]+/; + +function splitDraft(value: string): string[] { + const out: string[] = []; + for (const piece of value.split(SPLIT_RE)) { + const text = piece.trim(); + if (text) out.push(text); + } + return out; +} + +function appendUnique(existing: string[], incoming: string[]): string[] { + const seen = new Set(existing); + const next = [...existing]; + for (const value of incoming) { + if (seen.has(value)) continue; + seen.add(value); + next.push(value); + } + return next; +} + +export function IdListEditor({ + label, + values, + onChange, + placeholder = "输入后回车添加", + emptyHint = "尚未添加任何条目", + inputMode = "numeric", + disabled = false, + className, + itemAriaLabel, +}: IdListEditorProps): React.JSX.Element { + const [draft, setDraft] = useState(""); + + const commitDraft = useCallback( + (raw: string): boolean => { + const pieces = splitDraft(raw); + if (pieces.length === 0) return false; + const next = appendUnique(values, pieces); + if (next.length !== values.length) { + onChange(next); + } + return true; + }, + [onChange, values], + ); + + const handleAdd = useCallback(() => { + if (commitDraft(draft)) setDraft(""); + }, [commitDraft, draft]); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + if (commitDraft(draft)) setDraft(""); + return; + } + if ( + event.key === "Backspace" && + draft.length === 0 && + values.length > 0 && + !event.nativeEvent.isComposing + ) { + event.preventDefault(); + const next = values.slice(0, -1); + onChange(next); + } + }, + [commitDraft, draft, onChange, values], + ); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const next = event.target.value; + // Auto-split on separator characters (comma / semicolon / whitespace). + if (SPLIT_RE.test(next) && next.trim().length > 0) { + const pieces = splitDraft(next); + if (pieces.length > 0) { + const merged = appendUnique(values, pieces); + if (merged.length !== values.length) onChange(merged); + } + setDraft(""); + return; + } + setDraft(next); + }, + [onChange, values], + ); + + const removeItem = useCallback( + (index: number) => { + const next = values.filter((_, idx) => idx !== index); + onChange(next); + }, + [onChange, values], + ); + + const commitItemEdit = useCallback( + (index: number, raw: string) => { + const trimmed = raw.trim(); + if (trimmed === values[index]) return; + if (trimmed.length === 0) { + removeItem(index); + return; + } + if (values.some((entry, idx) => idx !== index && entry === trimmed)) { + removeItem(index); + return; + } + const next = [...values]; + next[index] = trimmed; + onChange(next); + }, + [onChange, removeItem, values], + ); + + return ( +
+
+ {label} + 0 + ? "bg-primary/12 text-primary" + : "bg-muted text-muted-foreground", + )} + > + {values.length} 项 + +
+ + {values.length === 0 ? ( +

+ {emptyHint} +

+ ) : ( +
    + {values.map((value, index) => ( +
  • + commitItemEdit(index, event.target.value)} + onChange={(event) => { + const next = [...values]; + next[index] = event.target.value; + onChange(next); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + (event.target as HTMLInputElement).blur(); + } + }} + value={value} + /> + +
  • + ))} +
+ )} + +
+ { + if (commitDraft(draft)) setDraft(""); + }} + onChange={handleChange} + onKeyDown={handleKeyDown} + placeholder={placeholder} + value={draft} + /> + +
+

+ 支持回车添加、粘贴含逗号 / 空格 / 分号的批量内容会自动拆分;清空输入框时按删除键可移除最后一项。 +

+
+ ); +} diff --git a/src/renderer/src/components/app/InitializationWizard.tsx b/src/renderer/src/components/app/InitializationWizard.tsx new file mode 100644 index 0000000..0d53eee --- /dev/null +++ b/src/renderer/src/components/app/InitializationWizard.tsx @@ -0,0 +1,419 @@ +import { + Bot, + CheckCircle2, + Loader2, + RotateCcw, + ShieldAlert, + TerminalSquare, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { + DesktopSnapshot, + LogEntry, + PythonOverridesState, + PythonPackageSourcePreset, + ServiceDescriptor, +} from "@shared/contracts"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Kbd } from "@/components/ui/kbd"; +import { Progress } from "@/components/ui/progress"; +import { useShortcut } from "@/lib/use-shortcut"; + +interface InitializationWizardProps { + snapshot: DesktopSnapshot; + onSnapshot: (snapshot: DesktopSnapshot) => void; + onOpenTab: (tab: string) => void; +} + +const STARTUP_WIZARD_KEY = "maibot-startup-wizard-seen"; +const LOCAL_CHAT_USER_NAME_STORAGE_KEY = "maibot.localChat.userName"; +const AUTO_START_DELAY_MS = 2000; +const WEBUI_READY_TIMEOUT_MS = 90_000; +const WEBUI_READY_POLL_MS = 1000; +type WizardStep = "core" | "profile"; + +function messageFromError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function readStartupWizardSeen(): boolean { + try { + return localStorage.getItem(STARTUP_WIZARD_KEY) === "1"; + } catch { + return false; + } +} + +function markStartupWizardSeen(): void { + try { + localStorage.setItem(STARTUP_WIZARD_KEY, "1"); + } catch { + // Local storage can be unavailable in isolated previews. + } +} + +function readLocalUserName(): string { + try { + return localStorage.getItem(LOCAL_CHAT_USER_NAME_STORAGE_KEY) ?? ""; + } catch { + return ""; + } +} + +function saveLocalUserName(userName: string): void { + try { + localStorage.setItem(LOCAL_CHAT_USER_NAME_STORAGE_KEY, userName); + } catch { + // Local storage can be unavailable in isolated previews. + } +} + +function maibotServiceFrom(snapshot: DesktopSnapshot): ServiceDescriptor | undefined { + return snapshot.services?.find((service) => service.id === "maibot"); +} + +function dependencyLogs(entries: LogEntry[]): LogEntry[] { + return entries + .filter((entry) => entry.source === "maibot" && entry.stream === "system") + .filter((entry) => + entry.message.includes("startup dependency upgrade") || + entry.message.includes("dependency") || + entry.message.includes("pip"), + ) + .slice(-8); +} + +function serviceProgress(service: ServiceDescriptor | undefined, busy: boolean): number { + if (service?.status === "running" && service.health === "ready") { + return 100; + } + if (service?.status === "running") { + return service.health === "checking" ? 88 : 92; + } + if (service?.status === "starting" && service.detail?.includes("依赖检查完成")) { + return 72; + } + if (service?.status === "starting") { + return 42; + } + return busy ? 18 : 0; +} + +function wizardServiceDetail(service: ServiceDescriptor | undefined, busy: boolean): string { + if (service?.status === "running" && service.health === "ready") { + return "MaiCore 已启动,正在进入首次配置"; + } + if (service?.status === "running" || service?.health === "unreachable") { + return "等待 MaiCore 启动中"; + } + if (service?.status === "starting") { + return service.detail ?? "正在启动 MaiCore"; + } + return busy ? "正在准备 MaiCore 启动环境" : "正在读取依赖源并准备自动初始化"; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +export function InitializationWizard({ + snapshot, + onSnapshot, + onOpenTab, +}: InitializationWizardProps): React.JSX.Element | null { + const [seen, setSeen] = useState(readStartupWizardSeen); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [pythonDeps, setPythonDeps] = useState(null); + const [localUserName, setLocalUserName] = useState(readLocalUserName); + const [step, setStep] = useState("core"); + const autoStartRequested = useRef(false); + const autoStartTimer = useRef | null>(null); + const agreementPending = !snapshot.startupAgreement.isConfirmed; + const service = maibotServiceFrom(snapshot); + const logs = useMemo(() => dependencyLogs(snapshot.recentLogs ?? []), [snapshot.recentLogs]); + const running = service?.status === "running"; + const ready = running && service?.health === "ready"; + const starting = service?.status === "starting"; + const open = !agreementPending && !seen; + const progress = serviceProgress(service, busy); + + const refreshSnapshot = useCallback(async () => { + const nextSnapshot = await window.maibotDesktop?.getSnapshot(); + if (nextSnapshot) { + onSnapshot(nextSnapshot); + } + return nextSnapshot; + }, [onSnapshot]); + + const waitForMaiBotWebUi = useCallback(async (): Promise => { + const startedAt = Date.now(); + while (Date.now() - startedAt < WEBUI_READY_TIMEOUT_MS) { + const nextSnapshot = await refreshSnapshot(); + const nextService = nextSnapshot ? maibotServiceFrom(nextSnapshot) : undefined; + if (nextService?.status === "running" && nextService.health === "ready") { + return; + } + if (nextService?.status === "error") { + throw new Error(nextService.error ?? nextService.detail ?? "MaiBot Core 鍚姩澶辫触"); + } + await delay(WEBUI_READY_POLL_MS); + } + + throw new Error("MaiBot WebUI 鍚姩瓒呮椂锛岃鍦ㄧ粓绔〉鏌ョ湅鍚姩鏃ュ織"); + }, [refreshSnapshot]); + + const close = useCallback(() => { + markStartupWizardSeen(); + setSeen(true); + }, []); + + const startMaiCore = useCallback(async () => { + setBusy(true); + setError(null); + try { + await window.maibotDesktop?.init.repair(); + await window.maibotDesktop?.services.start("maibot"); + await waitForMaiBotWebUi(); + setStep("profile"); + } catch (nextError) { + setError(messageFromError(nextError)); + await refreshSnapshot().catch(() => undefined); + } finally { + setBusy(false); + } + }, [refreshSnapshot, waitForMaiBotWebUi]); + + const saveDependencySource = useCallback(async (preset: PythonPackageSourcePreset) => { + setError(null); + if (!busy && !starting && !running) { + autoStartRequested.current = false; + if (autoStartTimer.current) { + clearTimeout(autoStartTimer.current); + autoStartTimer.current = null; + } + } + try { + const state = await window.maibotDesktop?.pythonDeps.saveSourcePreset(preset); + if (state) { + setPythonDeps(state); + } + } catch (nextError) { + setError(messageFromError(nextError)); + } + }, [busy, running, starting]); + + const updateLocalUserName = useCallback((value: string) => { + setLocalUserName(value); + setError(null); + }, []); + + useEffect(() => { + if (!open) { + return; + } + + let mounted = true; + window.maibotDesktop?.pythonDeps + .getState() + .then((state) => { + if (mounted) { + setPythonDeps(state); + } + }) + .catch((nextError: unknown) => { + if (mounted) { + setError(messageFromError(nextError)); + } + }); + + return () => { + mounted = false; + }; + }, [open]); + + useEffect(() => { + if (!open || step !== "core" || !pythonDeps || autoStartRequested.current || busy || starting || running) { + return; + } + + autoStartRequested.current = true; + autoStartTimer.current = setTimeout(() => { + void startMaiCore(); + }, AUTO_START_DELAY_MS); + + return () => { + if (autoStartTimer.current) { + clearTimeout(autoStartTimer.current); + autoStartTimer.current = null; + } + }; + }, [busy, open, pythonDeps, running, startMaiCore, starting, step]); + + const retry = useCallback(() => { + autoStartRequested.current = true; + void startMaiCore(); + }, [startMaiCore]); + + const finishProfile = useCallback(() => { + const userName = localUserName.trim(); + if (!userName) { + setError("请先填写你自己的用户名。"); + return; + } + saveLocalUserName(userName); + onOpenTab("maibot"); + close(); + }, [close, localUserName, onOpenTab]); + + useShortcut("Escape", close, { enabled: open && !busy, allowInEditable: true }); + + return ( + { if (!next && !busy) close(); }}> + { + if (busy) { + event.preventDefault(); + } + }} + size="lg" + > + } + title="初始化 MaiBot Core" + tone="default" + /> + + + {step === "core" ? ( + <> +
+
+
+ + + +
+

MaiBot Core

+

+ 首次启动会检查运行目录、同步基础文件,并按需安装 Python 覆盖依赖。 +

+
+
+ + {ready ? "WebUI 已就绪" : running ? "等待 WebUI" : starting || busy ? "初始化中" : "即将开始"} + +
+ + + +
+ +

+ {wizardServiceDetail(service, busy)} +

+
+
+ +
+
+ {ready ? : } + 依赖安装进度 +
+
+ {logs.length > 0 ? ( + logs.map((entry) => ( +

+ {entry.message} +

+ )) + ) : ( +

+ 还没有依赖安装日志。初始化开始后,检查、下载和安装输出会出现在这里。 +

+ )} +
+
+ + ) : ( +
+
+ + + +
+

设置你的用户名

+

+ 这个名称会用于随便聊聊里显示你的本地发送者名称。 +

+
+
+ +
+ )} + + {error ? ( +
+ + {error} +
+ ) : null} +
+ + + + {step === "core" && error && !busy && !starting ? ( + + ) : null} + {step === "profile" ? ( + + ) : null} + +
+
+ ); +} diff --git a/src/renderer/src/components/app/LocalChatPanel.tsx b/src/renderer/src/components/app/LocalChatPanel.tsx new file mode 100644 index 0000000..6683812 --- /dev/null +++ b/src/renderer/src/components/app/LocalChatPanel.tsx @@ -0,0 +1,1082 @@ +import { + Bot, + CircleAlert, + ChevronDown, + FileText, + ImageIcon, + Loader2, + MessageSquare, + Mic, + Square, + Paperclip, + RefreshCw, + Send, + Settings, + Smile, + UserRound, + X, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { + LocalChatEvent, + LocalChatFileAttachment, + LocalChatImageAttachment, + LocalChatMessageEvent, + LocalChatVoiceAttachment, + ServiceDescriptor, +} from "@shared/contracts"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { localChatErrorMessage } from "@/lib/local-chat-error"; +import { cn } from "@/lib/utils"; + +type ConnectionState = "idle" | "connecting" | "connected" | "error"; + +interface ChatMessage { + id: string; + role: "user" | "bot" | "system" | "error"; + content: string; + timestamp: number; + sender?: string; + images?: LocalChatImageAttachment[]; + emojis?: LocalChatImageAttachment[]; + files?: LocalChatFileAttachment[]; + voices?: LocalChatVoiceAttachment[]; + quote?: LocalChatMessageEvent["quote"]; + kind?: "chat" | "planner"; + final?: boolean; + collapsed?: boolean; + plannerTools?: LocalChatMessageEvent["plannerTools"]; +} + +const DEFAULT_USER_NAME = "本地用户"; +const USER_NAME_STORAGE_KEY = "maibot.localChat.userName"; +const USER_AVATAR_STORAGE_KEY = "maibot.localChat.userAvatar"; +const BOT_AVATAR_STORAGE_KEY = "maibot.localChat.botAvatar"; +const PLANNER_VISIBLE_STORAGE_KEY = "maibot.localChat.showPlanner"; +const MAX_IMAGE_BYTES = 8 * 1024 * 1024; +const MAX_FILE_BYTES = 16 * 1024 * 1024; +const MAX_VOICE_BYTES = 16 * 1024 * 1024; + +function maibotOrigin(service: ServiceDescriptor | undefined): string { + try { + return new URL(service?.url ?? "http://127.0.0.1:8001").origin; + } catch { + return "http://127.0.0.1:8001"; + } +} + +function toChatMessage(event: LocalChatMessageEvent): ChatMessage { + return { + id: event.id, + role: event.role, + content: event.content, + timestamp: event.timestamp, + sender: event.sender, + images: event.images, + emojis: event.emojis, + files: event.files, + voices: event.voices, + quote: event.quote, + kind: event.kind, + final: event.final, + collapsed: event.kind === "planner" ? true : undefined, + plannerTools: event.plannerTools, + }; +} + +function appendMessage(messages: ChatMessage[], message: ChatMessage): ChatMessage[] { + const existingIndex = messages.findIndex((item) => item.id === message.id); + let nextMessages: ChatMessage[]; + if (existingIndex >= 0) { + nextMessages = messages.map((item, index) => + index === existingIndex + ? { + ...item, + ...message, + collapsed: message.kind === "planner" ? item.collapsed ?? true : message.collapsed, + } + : item, + ); + } else { + nextMessages = [...messages, message].slice(-120); + } + return placePlannerBeforeReply(nextMessages); +} + +function placePlannerBeforeReply(messages: ChatMessage[]): ChatMessage[] { + const nextMessages = [...messages]; + for (let index = 0; index < nextMessages.length; index += 1) { + const message = nextMessages[index]; + if (message.kind !== "planner") { + continue; + } + + const userIndex = findPreviousUserIndex(nextMessages, index); + const firstReplyBeforePlanner = nextMessages.findIndex((item, itemIndex) => + itemIndex > userIndex && itemIndex < index && item.role === "bot" && item.kind !== "planner" + ); + if (firstReplyBeforePlanner < 0) { + continue; + } + + const planner = nextMessages.splice(index, 1)[0]; + const insertIndex = findPlannerInsertEnd(nextMessages, userIndex, firstReplyBeforePlanner); + nextMessages.splice(insertIndex, 0, planner); + index = Math.max(insertIndex, 0); + } + return nextMessages.slice(-120); +} + +function findPreviousUserIndex(messages: ChatMessage[], beforeIndex: number): number { + for (let index = Math.min(beforeIndex - 1, messages.length - 1); index >= 0; index -= 1) { + if (messages[index].role === "user") { + return index; + } + } + return -1; +} + +function findPlannerInsertEnd(messages: ChatMessage[], userIndex: number, beforeReplyIndex: number): number { + let insertIndex = userIndex + 1; + while (insertIndex < beforeReplyIndex && messages[insertIndex].kind === "planner") { + insertIndex += 1; + } + return insertIndex; +} + +function attachmentDataUrl(image: LocalChatImageAttachment): string { + return image.dataUrl ?? `data:${image.mimeType};base64,${image.base64}`; +} + +function voiceDataUrl(voice: LocalChatVoiceAttachment): string { + return voice.dataUrl ?? `data:${voice.mimeType};base64,${voice.base64}`; +} + +function readPlannerVisible(): boolean { + return localStorage.getItem(PLANNER_VISIBLE_STORAGE_KEY) !== "0"; +} + +function readImageFile(file: File): Promise { + return new Promise((resolve, reject) => { + if (!file.type.startsWith("image/")) { + reject(new Error("请选择图片文件")); + return; + } + if (file.size > MAX_IMAGE_BYTES) { + reject(new Error("图片不能超过 8 MB")); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = String(reader.result ?? ""); + const base64 = dataUrl.slice(dataUrl.indexOf(";base64,") + 8); + resolve({ name: file.name, mimeType: file.type, base64, dataUrl, size: file.size }); + }; + reader.onerror = () => reject(new Error("读取图片失败")); + reader.readAsDataURL(file); + }); +} + +function readRegularFile(file: File): Promise { + return new Promise((resolve, reject) => { + if (file.size > MAX_FILE_BYTES) { + reject(new Error("文件不能超过 16 MB")); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = String(reader.result ?? ""); + const base64 = dataUrl.slice(dataUrl.indexOf(";base64,") + 8); + resolve({ + name: file.name, + mimeType: file.type || "application/octet-stream", + base64, + size: file.size, + }); + }; + reader.onerror = () => reject(new Error("读取文件失败")); + reader.readAsDataURL(file); + }); +} + +function readVoiceBlob(blob: Blob, mimeType: string): Promise { + return new Promise((resolve, reject) => { + if (blob.size > MAX_VOICE_BYTES) { + reject(new Error("语音不能超过 16 MB")); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = String(reader.result ?? ""); + const base64 = dataUrl.slice(dataUrl.indexOf(";base64,") + 8); + resolve({ + name: `voice-${new Date().toISOString().replace(/[:.]/gu, "-")}.webm`, + mimeType, + base64, + dataUrl, + size: blob.size, + }); + }; + reader.onerror = () => reject(new Error("读取语音失败")); + reader.readAsDataURL(blob); + }); +} + +function preferredVoiceMimeType(): string { + if (typeof MediaRecorder === "undefined") { + return ""; + } + const candidates = ["audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4"]; + return candidates.find((candidate) => MediaRecorder.isTypeSupported(candidate)) ?? ""; +} + +function formatBytes(size: number): string { + if (!Number.isFinite(size) || size <= 0) { + return "未知大小"; + } + if (size < 1024) { + return `${size} B`; + } + if (size < 1024 * 1024) { + return `${(size / 1024).toFixed(1)} KB`; + } + return `${(size / 1024 / 1024).toFixed(1)} MB`; +} + +function AvatarButton({ + avatar, + fallback, + icon, + onPick, + title, +}: { + avatar: string; + fallback: React.ReactNode; + icon: React.ReactNode; + onPick: (file: File) => void; + title: string; +}): React.JSX.Element { + const inputRef = useRef(null); + + return ( + <> + + { + const file = event.target.files?.[0]; + if (file) { + onPick(file); + } + event.target.value = ""; + }} + ref={inputRef} + type="file" + /> + {icon} + + ); +} + +export function LocalChatPanel({ + active, + maibotService, +}: { + active: boolean; + maibotService: ServiceDescriptor | undefined; +}): React.JSX.Element { + const origin = useMemo(() => maibotOrigin(maibotService), [maibotService]); + const listRef = useRef(null); + const imageInputRef = useRef(null); + const emojiInputRef = useRef(null); + const fileInputRef = useRef(null); + const recorderRef = useRef(null); + const recordingStreamRef = useRef(null); + const recordingChunksRef = useRef([]); + const [state, setState] = useState("idle"); + const [error, setError] = useState(null); + const [messages, setMessages] = useState([]); + const [draft, setDraft] = useState(""); + const [userName, setUserName] = useState(() => localStorage.getItem(USER_NAME_STORAGE_KEY) ?? DEFAULT_USER_NAME); + const [userAvatar, setUserAvatar] = useState(() => localStorage.getItem(USER_AVATAR_STORAGE_KEY) ?? ""); + const [botAvatar, setBotAvatar] = useState(() => localStorage.getItem(BOT_AVATAR_STORAGE_KEY) ?? ""); + const [pendingImages, setPendingImages] = useState([]); + const [pendingEmojis, setPendingEmojis] = useState([]); + const [pendingFiles, setPendingFiles] = useState([]); + const [pendingVoices, setPendingVoices] = useState([]); + const [isTyping, setIsTyping] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const [showPlanner, setShowPlanner] = useState(readPlannerVisible); + const [recording, setRecording] = useState(false); + + useEffect(() => { + localStorage.setItem(USER_NAME_STORAGE_KEY, userName); + }, [userName]); + + useEffect(() => { + localStorage.setItem(PLANNER_VISIBLE_STORAGE_KEY, showPlanner ? "1" : "0"); + }, [showPlanner]); + + useEffect(() => { + if (userAvatar) { + localStorage.setItem(USER_AVATAR_STORAGE_KEY, userAvatar); + } else { + localStorage.removeItem(USER_AVATAR_STORAGE_KEY); + } + }, [userAvatar]); + + useEffect(() => { + if (botAvatar) { + localStorage.setItem(BOT_AVATAR_STORAGE_KEY, botAvatar); + } else { + localStorage.removeItem(BOT_AVATAR_STORAGE_KEY); + } + }, [botAvatar]); + + const connect = useCallback(async () => { + setState("connecting"); + setError(null); + setIsTyping(false); + + try { + if (maibotService?.status !== "running") { + throw new Error("请先启动 MaiBot Core"); + } + + const nextState = await window.maibotDesktop?.localChat.connect(); + setState(nextState ?? "error"); + const history = await window.maibotDesktop?.localChat.listMessages(); + if (history) { + setMessages(history.map(toChatMessage)); + } + if (nextState !== "connected") { + setError("MaiBot Core 正在启动或 WebUI 聊天服务还在加载,请稍等片刻后重试。"); + } + } catch (nextError) { + setState("error"); + setError(localChatErrorMessage(nextError)); + } + }, [maibotService?.status]); + + useEffect(() => { + if (!active) { + setState("idle"); + setIsTyping(false); + return undefined; + } + + const bridge = window.maibotDesktop?.localChat; + const unsubscribe = bridge?.onEvent((event: LocalChatEvent) => { + if ("type" in event) { + setState(event.state); + if (event.state === "connected") { + setError(null); + } + return; + } + + setIsTyping(false); + const message = toChatMessage(event); + setMessages((current) => appendMessage(current, message)); + }); + + void connect(); + return () => { + unsubscribe?.(); + }; + }, [active, connect]); + + useEffect(() => { + listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" }); + }, [messages, isTyping]); + + const pickAvatar = useCallback(async (file: File, target: "bot" | "user") => { + try { + const image = await readImageFile(file); + if (target === "bot") { + setBotAvatar(attachmentDataUrl(image)); + } else { + setUserAvatar(attachmentDataUrl(image)); + } + setError(null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + } + }, []); + + const addImages = useCallback(async (files: FileList | null) => { + if (!files?.length) { + return; + } + try { + const images = await Promise.all(Array.from(files).map(readImageFile)); + setPendingImages((current) => [...current, ...images].slice(0, 6)); + setError(null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + } + }, []); + + const addEmojis = useCallback(async (files: FileList | null) => { + if (!files?.length) { + return; + } + try { + const emojis = await Promise.all(Array.from(files).map(readImageFile)); + setPendingEmojis((current) => [...current, ...emojis].slice(0, 12)); + setError(null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + } + }, []); + + const addFiles = useCallback(async (files: FileList | null) => { + if (!files?.length) { + return; + } + try { + const nextFiles = await Promise.all(Array.from(files).map(readRegularFile)); + setPendingFiles((current) => [...current, ...nextFiles].slice(0, 6)); + setError(null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + } + }, []); + + const stopRecordingTracks = useCallback(() => { + recordingStreamRef.current?.getTracks().forEach((track) => track.stop()); + recordingStreamRef.current = null; + }, []); + + const stopVoiceRecording = useCallback(() => { + const recorder = recorderRef.current; + if (!recorder) { + setRecording(false); + return; + } + try { + if (recorder.state !== "inactive") { + recorder.stop(); + } + } catch (nextError) { + setRecording(false); + stopRecordingTracks(); + setError(nextError instanceof Error ? nextError.message : String(nextError)); + } + }, [stopRecordingTracks]); + + const startVoiceRecording = useCallback(async () => { + if (recording) { + stopVoiceRecording(); + return; + } + if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") { + setError("当前环境不支持录音"); + return; + } + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const mimeType = preferredVoiceMimeType(); + const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream); + recordingStreamRef.current = stream; + recordingChunksRef.current = []; + recorderRef.current = recorder; + + recorder.ondataavailable = (event) => { + if (event.data.size > 0) { + recordingChunksRef.current.push(event.data); + } + }; + recorder.onerror = () => { + setError("录音失败"); + setRecording(false); + stopRecordingTracks(); + }; + recorder.onstop = () => { + const recordedMimeType = recorder.mimeType || mimeType || "audio/webm"; + const blob = new Blob(recordingChunksRef.current, { type: recordedMimeType }); + recorderRef.current = null; + recordingChunksRef.current = []; + stopRecordingTracks(); + setRecording(false); + if (blob.size <= 0) { + setError("没有录到语音内容"); + return; + } + void readVoiceBlob(blob, recordedMimeType) + .then((voice) => { + setPendingVoices((current) => [...current, voice].slice(0, 4)); + setError(null); + }) + .catch((nextError) => { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + }); + }; + + recorder.start(); + setRecording(true); + setError(null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + setRecording(false); + stopRecordingTracks(); + } + }, [recording, stopRecordingTracks, stopVoiceRecording]); + + useEffect(() => () => { + recorderRef.current?.state === "recording" && recorderRef.current.stop(); + stopRecordingTracks(); + }, [stopRecordingTracks]); + + const sendMessage = useCallback(async () => { + const content = draft.trim(); + if ( + (!content && pendingImages.length === 0 && pendingEmojis.length === 0 && pendingFiles.length === 0 && pendingVoices.length === 0) + || state !== "connected" + ) { + return; + } + const images = pendingImages; + const emojis = pendingEmojis; + const files = pendingFiles; + const voices = pendingVoices; + setDraft(""); + setPendingImages([]); + setPendingEmojis([]); + setPendingFiles([]); + setPendingVoices([]); + setIsTyping(true); + + try { + const sent = await window.maibotDesktop?.localChat.send({ content, images, emojis, files, voices, userName }); + if (sent) { + setMessages((current) => appendMessage(current, toChatMessage(sent))); + } + } catch (nextError) { + setIsTyping(false); + setState("error"); + setError(nextError instanceof Error ? nextError.message : String(nextError)); + setDraft(content); + setPendingImages(images); + setPendingEmojis(emojis); + setPendingFiles(files); + setPendingVoices(voices); + } + }, [draft, pendingEmojis, pendingFiles, pendingImages, pendingVoices, state, userName]); + + const connected = state === "connected"; + const canSend = connected && ( + draft.trim() + || pendingImages.length > 0 + || pendingEmojis.length > 0 + || pendingFiles.length > 0 + || pendingVoices.length > 0 + ); + + return ( + <> +
+
+
+ +

随便聊聊

+ + {connected ? "已连接" : state === "connecting" ? "连接中" : state === "error" ? "未连接" : "待连接"} + + + {origin} / simple-chat + +
+
+ + + +
+
+ + {error ? ( +
+ + {error} +
+ ) : null} + +
+
+ {messages.length === 0 ? ( +
+
+ +

和 MaiBot 本地对话

+

启动 MaiBot Core 后通过内置聊天通道发送消息。

+
+
+ ) : ( + messages.filter((message) => showPlanner || message.kind !== "planner").map((message) => ( +
+ {message.role !== "user" ? ( + + {message.role === "bot" && botAvatar ? ( + + ) : message.role === "bot" ? ( + + ) : message.kind === "planner" ? ( + + ) : ( + + )} + + ) : null} +
+ {message.sender ?

{message.sender}

: null} + {message.quote ? ( +
+ {message.quote.sender ? ( +

{message.quote.sender}

+ ) : null} +

{message.quote.content}

+
+ ) : null} + {message.kind === "planner" ? ( +
+ {message.content ? ( +

+ {message.content} +

+ ) : null} + {message.collapsed ? ( +
+ ) : null} +
+ ) : message.content ? ( +

{message.content}

+ ) : null} + {message.kind === "planner" && !message.collapsed && message.plannerTools?.length ? ( +
+ {message.plannerTools.map((tool, index) => ( +
+
+ {tool.name} +
+ {typeof tool.success === "boolean" ? ( + + {tool.success ? "成功" : "失败"} + + ) : null} + {typeof tool.durationMs === "number" ? ( + {Math.round(tool.durationMs)} ms + ) : null} +
+
+ {tool.arguments?.length ? ( +
+ {tool.arguments.map((argument) => ( + + {argument.key} + {argument.value} + + ))} +
+ ) : tool.argumentsText ? ( + + 参数 + {tool.argumentsText} + + ) : null} + {tool.resultText ? ( +

+ {tool.resultText} +

+ ) : null} +
+ ))} +
+ ) : null} + {message.kind === "planner" && (message.content || message.plannerTools?.length) ? ( + + ) : null} + {message.images?.length ? ( +
+ {message.images.map((image, index) => ( + {image.name + ))} +
+ ) : null} + {message.emojis?.length ? ( +
+ {message.emojis.map((emoji, index) => ( + {emoji.name + ))} +
+ ) : null} + {message.files?.length ? ( +
+ {message.files.map((file, index) => ( +
+ +
+

{file.name}

+

{formatBytes(file.size)}

+
+
+ ))} +
+ ) : null} + {message.voices?.length ? ( +
+ {message.voices.map((voice, index) => ( +
+ +
+ ))} +
+ ) : null} +
+ {message.role === "user" ? ( + + {userAvatar ? : } + + ) : null} +
+ )) + )} + {isTyping ? ( +
+ + MaiBot 正在思考 +
+ ) : null} +
+
+ +
+
+ {pendingImages.length ? ( +
+ {pendingImages.map((image, index) => ( +
+ {image.name + +
+ ))} +
+ ) : null} + {pendingEmojis.length ? ( +
+ {pendingEmojis.map((emoji, index) => ( +
+ {emoji.name + +
+ ))} +
+ ) : null} + {pendingFiles.length ? ( +
+ {pendingFiles.map((file, index) => ( +
+ +
+

{file.name}

+

{formatBytes(file.size)}

+
+ +
+ ))} +
+ ) : null} + {pendingVoices.length ? ( +
+ {pendingVoices.map((voice, index) => ( +
+ +
+ ))} +
+ ) : null} +
+ + { + void addImages(event.target.files); + event.target.value = ""; + }} + ref={imageInputRef} + type="file" + /> + + { + void addEmojis(event.target.files); + event.target.value = ""; + }} + ref={emojiInputRef} + type="file" + /> + + { + void addFiles(event.target.files); + event.target.value = ""; + }} + ref={fileInputRef} + type="file" + /> + +