diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index f48788c5..50c6617b 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -90,20 +90,20 @@ jobs: ### Windows (不再支持Win7) #### 正常版本(推荐) - - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) ### macOS - - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg) ### Linux #### DEB包(Debian系) 使用 apt ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) #### RPM包(Redhat系) 使用 dnf ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) @@ -169,7 +169,8 @@ jobs: workspaces: src-tauri cache-all-crates: true save-if: ${{ github.ref == 'refs/heads/dev' }} - shared-key: autobuild-shared + shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -197,6 +198,14 @@ jobs: node-version: "22" cache: "pnpm" + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + - name: Pnpm install and check run: | pnpm i @@ -259,7 +268,8 @@ jobs: workspaces: src-tauri cache-all-crates: true save-if: ${{ github.ref == 'refs/heads/dev' }} - shared-key: autobuild-shared + shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} - name: Install pnpm uses: pnpm/action-setup@v4 @@ -272,6 +282,14 @@ jobs: node-version: "22" cache: "pnpm" + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + - name: Pnpm install and check run: | pnpm i @@ -391,7 +409,8 @@ jobs: workspaces: src-tauri cache-all-crates: true save-if: ${{ github.ref == 'refs/heads/dev' }} - shared-key: autobuild-shared + shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} - name: Install pnpm uses: pnpm/action-setup@v4 @@ -404,6 +423,14 @@ jobs: node-version: "22" cache: "pnpm" + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + - name: Pnpm install and check run: | pnpm i @@ -538,20 +565,20 @@ jobs: ### Windows (不再支持Win7) #### 正常版本(推荐) - - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe) + - [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe) #### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe) ### macOS - - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg) + - [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg) ### Linux #### DEB包(Debian系) 使用 apt ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) #### RPM包(Redhat系) 使用 dnf ./路径 安装 - - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) + - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm) ### FAQ - [常见问题](https://clash-verge-rev.github.io/faq/windows.html) diff --git a/.github/workflows/lint-clippy.yml b/.github/workflows/lint-clippy.yml index 38ca85c0..d7f3416b 100644 --- a/.github/workflows/lint-clippy.yml +++ b/.github/workflows/lint-clippy.yml @@ -59,9 +59,10 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri + cache-all-crates: true save-if: false - cache-all-crates: false - shared-key: autobuild-shared + shared-key: autobuild-${{ runner.os }}-${{ matrix.target }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }} - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -72,3 +73,10 @@ jobs: - name: Run Clippy working-directory: ./src-tauri run: cargo clippy-all + + - name: Run Logging Check + working-directory: ./src-tauri + shell: bash + run: | + cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git + clash-verge-logging-check diff --git a/.gitignore b/.gitignore index 96587689..9983a234 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ scripts/_env.sh .idea .old .eslintcache +target \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 77736fde..ee8dc1ce 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -27,6 +27,11 @@ if [ -n "$RUST_FILES" ]; then ( cd src-tauri cargo clippy-all + if ! command -v clash-verge-logging-check >/dev/null 2>&1; then + echo "[pre-commit] Installing clash-verge-logging-check..." + cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git + fi + clash-verge-logging-check ) fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b55dd91..e23cd352 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,10 @@ Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing. +## Internationalization (i18n) + +We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs. + ## Development Setup Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow: diff --git a/UPDATELOG.md b/UPDATELOG.md index 3f9a27bf..2330e82d 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -1,6 +1,45 @@ ## v2.4.3 -### ✨ 新增功能 +感谢 @Slinetrac, @oomeow 以及 @Lythrilla 的出色贡献 + +### 🐞 修复问题 + +- 优化服务模式重装逻辑,避免不必要的重复检查 +- 修复轻量模式退出无响应的问题 +- 修复托盘轻量模式支持退出/进入 +- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 +- macOS Tun/系统代理 模式下图标大小不统一 +- 托盘节点切换不再显示隐藏组 +- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 +- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 +- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 +- 修复 Webdav 恢复备份不重启 +- 修复 Linux 开机后无法正常代理需要手动设置 +- 修复增加订阅或导入订阅文件时订阅页面无更新 +- 修复系统代理守卫功能不工作 +- 修复 KDE + Wayland 下多屏显示 UI 异常 +- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 +- 修复静默启动不加载完整 WebView 的问题 +- 修复 Linux WebKit 网络进程的崩溃 +- 修复无法导入订阅 +- 修复实际导入成功但显示导入失败的问题 +- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题 +- 修复删除订阅时未能实际删除相关文件 +- 修复 macOS 连接界面显示异常 +- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 +- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 +- 修复自动更新使版本回退的问题 +- 修复首页自定义卡片在切换轻量模式时失效 +- 修复悬浮跳转导航失效 +- 修复小键盘热键映射错误 +- 修复前端无法及时刷新操作状态 +- 修复 macOS 从 Dock 栏退出轻量模式状态不同步 +- 修复 Linux 系统主题切换不生效 +- 修复 `允许自动更新` 字段使手动订阅刷新失效 +- 修复连接界面长时间显示后报错问题 + +
+ ✨ 新增功能 - **Mihomo(Meta) 内核升级至 v1.19.15** - 支持前端修改日志(最大文件大小、最大保留数量) @@ -15,8 +54,11 @@ - 允许独立控制订阅自动更新 - 托盘 `更多` 中新增 `关闭所有连接` 按钮 - 新增左侧菜单栏的排序功能(右键点击左侧菜单栏) +- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志` +
-### 🚀 优化改进 +
+ 🚀 优化改进 - 重构并简化服务模式启动检测流程,消除重复检测 - 重构并简化窗口创建流程 @@ -42,37 +84,11 @@ - 优化首页当前节点对MATCH规则的支持 - 允许在 `界面设置` 修改 `悬浮跳转导航延迟` - 添加热键绑定错误的提示信息 +- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 +- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 +- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` -### 🐞 修复问题 - -- 优化服务模式重装逻辑,避免不必要的重复检查 -- 修复轻量模式退出无响应的问题 -- 修复托盘轻量模式支持退出/进入 -- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 -- macOS Tun/系统代理 模式下图标大小不统一 -- 托盘节点切换不再显示隐藏组 -- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 -- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 -- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 -- 修复 Webdav 恢复备份不重启 -- 修复 Linux 开机后无法正常代理需要手动设置 -- 修复增加订阅或导入订阅文件时订阅页面无更新 -- 修复系统代理守卫功能不工作 -- 修复 KDE + Wayland 下多屏显示 UI 异常 -- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 -- 修复静默启动不加载完整 WebView 的问题 -- 修复 Linux WebKit 网络进程的崩溃 -- 修复无法导入订阅 -- 修复实际导入成功但显示导入失败的问题 -- 修复删除订阅时未能实际删除相关文件 -- 修复 macOS 连接界面显示异常 -- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 -- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 -- 修复自动更新使版本回退的问题 -- 修复首页自定义卡片在切换轻量模式时失效 -- 修复悬浮跳转导航失效 -- 修复小键盘热键映射错误 -- 修复连接界面长时间显示后报错问题 +
## v2.4.2 diff --git a/docs/CONTRIBUTING_i18n.md b/docs/CONTRIBUTING_i18n.md new file mode 100644 index 00000000..6e97e463 --- /dev/null +++ b/docs/CONTRIBUTING_i18n.md @@ -0,0 +1,81 @@ +# CONTRIBUTING — i18n + +Thank you for considering contributing to our localization work — your help is appreciated. + +Quick overview + +- cvr-i18 is a CLI that helps manage simple top-level JSON locale files: + - Detect duplicated top-level keys + - Find keys missing versus a base file (default: en.json) + - Export missing entries for translators + - Reorder keys to match the base file for predictable diffs + - Operate on a directory or a single file + +Get the CLI (No binary provided yet) + +```bash +git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli +cd clash-verge-rev-i18n-cli +cargo install --path . +# or +cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli +``` + +Common commands + +- Show help: `cvr-i18` +- Directory (auto-detects `./locales` or `./src/locales`): `cvr-i18 -d /path/to/locales` +- Check duplicates: `cvr-i18 -k` +- Check missing keys: `cvr-i18 -m` +- Export missing keys: `cvr-i18 -m -e ./exports` +- Sort keys to base file: `cvr-i18 -s` +- Use a base file: `cvr-i18 -b base.json` +- Single file: `cvr-i18 -f locales/zh.json` + +Options (short) + +- `-d, --directory ` +- `-f, --file ` +- `-k, --duplicated-key` +- `-m, --missing-key` +- `-e, --export ` +- `-s, --sort` +- `-b, --base ` + +Exit codes + +- `0` — success (no issues) +- `1` — issues found (duplicates/missing) +- `2` — error (IO/parse/runtime) + +How to contribute (recommended steps) + +- Start small: fix typos, improve phrasing, or refine tone and consistency. +- Run the CLI against your locale files to detect duplicates or missing keys. +- Export starter JSONs for translators with `-m -e `. +- Prefer incremental PRs or draft PRs; leave a comment on the issue if you want guidance. +- Open an issue to report missing strings, UI context, or localization bugs. +- Add or improve docs and tests to make future contributions easier. + +PR checklist + +- Keep JSON files UTF-8 encoded. +- Follow the repo’s locale file structure and naming conventions. +- Reorder keys to match the base file (`-s`) for minimal diffs. +- Test translations in a local dev build before opening a PR. +- Reference related issues and explain any context for translations or changes. + +Notes + +- The tool expects simple top-level JSON key/value maps. +- Exported JSONs are starter files for translators (fill in values, keep keys). +- Sorting keeps diffs consistent and reviewable. + +Repository +https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli + +## Feedback & Contributions + +- For tool usage issues or feedback: please open an Issue in the [repository](https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli) so it can be tracked and addressed. +- For localization contributions (translations, fixes, context notes, etc.): submit a PR or Issue in this repository and include examples, context, and testing instructions when possible. +- If you need help or a review, leave a comment on your submission requesting assistance. diff --git a/eslint.config.ts b/eslint.config.ts index 8146350f..8342ff0f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -94,9 +94,10 @@ export default defineConfig([ "warn", { vars: "all", - varsIgnorePattern: "^_+$", + varsIgnorePattern: "^_", args: "after-used", - argsIgnorePattern: "^_+$", + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^ignore", }, ], diff --git a/package.json b/package.json index 0e892b64..4a4c3237 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "publish-version": "node scripts/publish-version.mjs", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml", - "lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src", - "lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src", + "lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src", + "lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src", "format": "prettier --write .", "format:check": "prettier --check .", "typecheck": "tsc --noEmit", @@ -45,17 +45,17 @@ "@mui/material": "^7.3.4", "@mui/x-data-grid": "^7.29.9", "@tauri-apps/api": "2.9.0", - "@tauri-apps/plugin-clipboard-manager": "^2.3.1", - "@tauri-apps/plugin-dialog": "^2.4.1", - "@tauri-apps/plugin-fs": "^2.4.3", - "@tauri-apps/plugin-http": "~2.5.3", - "@tauri-apps/plugin-process": "^2.3.0", - "@tauri-apps/plugin-shell": "2.3.2", + "@tauri-apps/plugin-clipboard-manager": "^2.3.2", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-fs": "^2.4.4", + "@tauri-apps/plugin-http": "~2.5.4", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-shell": "2.3.3", "@tauri-apps/plugin-updater": "2.9.0", "@types/json-schema": "^7.0.15", "ahooks": "^3.9.6", - "axios": "^1.12.2", - "dayjs": "1.11.18", + "axios": "^1.13.1", + "dayjs": "1.11.19", "foxact": "^0.2.49", "i18next": "^25.6.0", "js-yaml": "^4.1.0", @@ -67,34 +67,33 @@ "react": "19.2.0", "react-dom": "19.2.0", "react-error-boundary": "6.0.0", - "react-hook-form": "^7.65.0", - "react-i18next": "16.2.1", + "react-hook-form": "^7.66.0", + "react-i18next": "16.2.3", "react-markdown": "10.1.0", "react-monaco-editor": "0.59.0", - "react-router": "^7.9.4", + "react-router": "^7.9.5", "react-virtuoso": "^4.14.1", "swr": "^2.3.6", "tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo", - "types-pac": "^1.0.3", - "zustand": "^5.0.8" + "types-pac": "^1.0.3" }, "devDependencies": { "@actions/github": "^6.0.1", - "@eslint-react/eslint-plugin": "^2.2.4", - "@eslint/js": "^9.38.0", - "@tauri-apps/cli": "2.9.1", + "@eslint-react/eslint-plugin": "^2.3.1", + "@eslint/js": "^9.39.0", + "@tauri-apps/cli": "2.9.2", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.9.1", + "@types/node": "^24.9.2", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "@vitejs/plugin-legacy": "^7.2.1", - "@vitejs/plugin-react": "5.1.0", + "@vitejs/plugin-react-swc": "^4.2.0", "adm-zip": "^0.5.16", "cli-color": "^2.0.4", "commander": "^14.0.2", "cross-env": "^10.1.0", - "eslint": "^9.38.0", + "eslint": "^9.39.0", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import-x": "^4.16.1", @@ -103,7 +102,7 @@ "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-unused-imports": "^4.3.0", "glob": "^11.0.3", - "globals": "^16.4.0", + "globals": "^16.5.0", "https-proxy-agent": "^7.0.6", "husky": "^9.1.7", "jiti": "^2.6.1", @@ -111,19 +110,19 @@ "meta-json-schema": "^1.19.14", "node-fetch": "^3.3.2", "prettier": "^3.6.2", - "sass": "^1.93.2", - "tar": "^7.5.1", + "sass": "^1.93.3", + "tar": "^7.5.2", "terser": "^5.44.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2", "vite": "^7.1.12", - "vite-plugin-monaco-editor": "^1.1.0", + "vite-plugin-monaco-editor-esm": "^2.0.2", "vite-plugin-svgr": "^4.5.0", - "vitest": "^4.0.3" + "vitest": "^4.0.6" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ - "eslint --fix", + "eslint --fix --max-warnings=0", "prettier --write", "git add" ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15f49800..20d71152 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,23 +42,23 @@ importers: specifier: 2.9.0 version: 2.9.0 '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.3.2 + version: 2.3.2 + '@tauri-apps/plugin-dialog': + specifier: ^2.4.2 + version: 2.4.2 + '@tauri-apps/plugin-fs': + specifier: ^2.4.4 + version: 2.4.4 + '@tauri-apps/plugin-http': + specifier: ~2.5.4 + version: 2.5.4 + '@tauri-apps/plugin-process': specifier: ^2.3.1 version: 2.3.1 - '@tauri-apps/plugin-dialog': - specifier: ^2.4.1 - version: 2.4.1 - '@tauri-apps/plugin-fs': - specifier: ^2.4.3 - version: 2.4.3 - '@tauri-apps/plugin-http': - specifier: ~2.5.3 - version: 2.5.3 - '@tauri-apps/plugin-process': - specifier: ^2.3.0 - version: 2.3.0 '@tauri-apps/plugin-shell': - specifier: 2.3.2 - version: 2.3.2 + specifier: 2.3.3 + version: 2.3.3 '@tauri-apps/plugin-updater': specifier: 2.9.0 version: 2.9.0 @@ -69,11 +69,11 @@ importers: specifier: ^3.9.6 version: 3.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 dayjs: - specifier: 1.11.18 - version: 1.11.18 + specifier: 1.11.19 + version: 1.11.19 foxact: specifier: ^0.2.49 version: 0.2.49(react@19.2.0) @@ -108,11 +108,11 @@ importers: specifier: 6.0.0 version: 6.0.0(react@19.2.0) react-hook-form: - specifier: ^7.65.0 - version: 7.65.0(react@19.2.0) + specifier: ^7.66.0 + version: 7.66.0(react@19.2.0) react-i18next: - specifier: 16.2.1 - version: 16.2.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.2.3 + version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.2)(react@19.2.0) @@ -120,8 +120,8 @@ importers: specifier: 0.59.0 version: 0.59.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-router: - specifier: ^7.9.4 - version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^7.9.5 + version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-virtuoso: specifier: ^4.14.1 version: 4.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -134,22 +134,19 @@ importers: types-pac: specifier: ^1.0.3 version: 1.0.3 - zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) devDependencies: '@actions/github': specifier: ^6.0.1 version: 6.0.1 '@eslint-react/eslint-plugin': - specifier: ^2.2.4 - version: 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + specifier: ^2.3.1 + version: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@eslint/js': - specifier: ^9.38.0 - version: 9.38.0 + specifier: ^9.39.0 + version: 9.39.0 '@tauri-apps/cli': - specifier: 2.9.1 - version: 2.9.1 + specifier: 2.9.2 + version: 2.9.2 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -157,8 +154,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.9.1 - version: 24.9.1 + specifier: ^24.9.2 + version: 24.9.2 '@types/react': specifier: 19.2.2 version: 19.2.2 @@ -167,10 +164,10 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-legacy': specifier: ^7.2.1 - version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) - '@vitejs/plugin-react': - specifier: 5.1.0 - version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) + version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + '@vitejs/plugin-react-swc': + specifier: ^4.2.0 + version: 4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -184,35 +181,35 @@ importers: specifier: ^10.1.0 version: 10.1.0 eslint: - specifier: ^9.38.0 - version: 9.38.0(jiti@2.6.1) + specifier: ^9.39.0 + version: 9.39.0(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.38.0(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.0(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.38.0(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))(prettier@3.6.2) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.38.0(jiti@2.6.1)) + version: 7.0.1(eslint@9.39.0(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.4.24 - version: 0.4.24(eslint@9.38.0(jiti@2.6.1)) + version: 0.4.24(eslint@9.39.0(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)) glob: specifier: ^11.0.3 version: 11.0.3 globals: - specifier: ^16.4.0 - version: 16.4.0 + specifier: ^16.5.0 + version: 16.5.0 https-proxy-agent: specifier: ^7.0.6 version: 7.0.6 @@ -235,11 +232,11 @@ importers: specifier: ^3.6.2 version: 3.6.2 sass: - specifier: ^1.93.2 - version: 1.93.2 + specifier: ^1.93.3 + version: 1.93.3 tar: - specifier: ^7.5.1 - version: 7.5.1 + specifier: ^7.5.2 + version: 7.5.2 terser: specifier: ^5.44.0 version: 5.44.0 @@ -248,19 +245,19 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.46.2 - version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.12 - version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) - vite-plugin-monaco-editor: - specifier: ^1.1.0 - version: 1.1.0(monaco-editor@0.54.0) + version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) + vite-plugin-monaco-editor-esm: + specifier: ^2.0.2 + version: 2.0.2(monaco-editor@0.54.0) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) + version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) vitest: - specifier: ^4.0.3 - version: 4.0.3(@types/debug@4.1.12)(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + specifier: ^4.0.6 + version: 4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) packages: @@ -666,18 +663,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.28.1': resolution: {integrity: sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==} engines: {node: '>=6.9.0'} @@ -1025,59 +1010,59 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@2.2.4': - resolution: {integrity: sha512-kdG9yMJ2QpEbVPfgvlWqTUAF2L7dZYBAaF8/LPrjDIKB1pCbygxXUoRBPVthxTsE2XTqiLbhPqcWGnI8Q3UNTQ==} + '@eslint-react/ast@2.3.1': + resolution: {integrity: sha512-jB/P72HVbZcC7DtUvjna8tjPSageAS6L9x5muMsBRQxEXkfv2J6CPX47sSpaPu1mMJn1Zzpn9m5z4aTPbfV6Ug==} engines: {node: '>=20.19.0'} - '@eslint-react/core@2.2.4': - resolution: {integrity: sha512-uEfUX2GoIymsBbWccJGpuyz8KCtxyNBxJb2FMyqE37nLtNVPbNsFTHRr6uX1WwkBxw+bUOYDbVDy9zFVbmAJXA==} + '@eslint-react/core@2.3.1': + resolution: {integrity: sha512-R0gXjIqHqqYSeHxNMbXblnlwzmZ2gD32aVPmrJB+SrLP0rItzo/WgVSvstjOK5+N5KExdM87hopFcqnlZS3ONg==} engines: {node: '>=20.19.0'} - '@eslint-react/eff@2.2.4': - resolution: {integrity: sha512-I26FQr5IEjJDXlcuyL1h/shmUdyyAXZrG+Op/E0Lc6cpGvXg5hn1ptcdKJ23o8BAxq2UY2gwyltGxE2t4ixoJQ==} + '@eslint-react/eff@2.3.1': + resolution: {integrity: sha512-k58lxHmhzatRZXVFzWdLZwfRwPgI5Thhf7BNVJ9F+NI2G1fFypclSVFRPqjGmI5jQ8bqB+5UVt9Rh49rZGZPzw==} engines: {node: '>=20.19.0'} - '@eslint-react/eslint-plugin@2.2.4': - resolution: {integrity: sha512-hGWCliK90mqF7Wd3TCBT/BtOgdWsyJ/Y+Zoor7Oz3fI7mu7MvqpBkGerIRCkbQxPthKj4/g2S1CEltvQ2jM4Sg==} + '@eslint-react/eslint-plugin@2.3.1': + resolution: {integrity: sha512-ThWx+AWI3Tl/6g+L1Cq/kTQrrZ4NXWMxRN92iBswYMW7bPaolh/8WBdiLAVZldqnlm+l6LZriia89jyr0CeTHA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - '@eslint-react/shared@2.2.4': - resolution: {integrity: sha512-jDL17njTyVj/cmveNThHtLLJpHqLRd/z76q+38Zcq+kiA3DfZ8mXyy+EYV4lLwD9dvg1FOMomHBTgV/woqWsRQ==} + '@eslint-react/shared@2.3.1': + resolution: {integrity: sha512-UiTbPi1i7UPdsIT2Z7mKZ3zzrgAm1GLeexkKe4QwvZJ1LLeEJmgMwHUw852+VzlDeV8stcQmZ9zWqFX2L0CmGg==} engines: {node: '>=20.19.0'} - '@eslint-react/var@2.2.4': - resolution: {integrity: sha512-MBh64lfHI6Cr2qjaYlJx7x3FcYqgGK9SSB5/7weRsxv63ZfGiJY+aRi0ahSGsE2JhM0/OhWu0T6T1z4nnEbQxA==} + '@eslint-react/var@2.3.1': + resolution: {integrity: sha512-1rC9dbuKKMq77pPoODGT91VTA3ReivIAfdFJePEjscPSRAUhCy7QPA/yK8MPe9nTsG89IDV+hilCGKiLZW8vNQ==} engines: {node: '>=20.19.0'} '@eslint/config-array@0.21.1': resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.4.1': - resolution: {integrity: sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.16.0': - resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.38.0': - resolution: {integrity: sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==} + '@eslint/js@9.39.0': + resolution: {integrity: sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.0': - resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fastify/busboy@2.1.1': @@ -1614,97 +1599,172 @@ packages: peerDependencies: '@svgr/core': '*' + '@swc/core-darwin-arm64@1.14.0': + resolution: {integrity: sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.14.0': + resolution: {integrity: sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.14.0': + resolution: {integrity: sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.14.0': + resolution: {integrity: sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.14.0': + resolution: {integrity: sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.14.0': + resolution: {integrity: sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.14.0': + resolution: {integrity: sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.14.0': + resolution: {integrity: sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.14.0': + resolution: {integrity: sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.14.0': + resolution: {integrity: sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.14.0': + resolution: {integrity: sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@tauri-apps/api@2.9.0': resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==} - '@tauri-apps/cli-darwin-arm64@2.9.1': - resolution: {integrity: sha512-sdwhtsE/6njD0AjgfYEj1JyxZH4SBmCJSXpRm6Ph5fQeuZD6MyjzjdVOrrtFguyREVQ7xn0Ujkwvbo01ULthNg==} + '@tauri-apps/cli-darwin-arm64@2.9.2': + resolution: {integrity: sha512-g1OtCXydOZFYRUEAyGYdJ2lLaE3l5jk8o+Bro8y2WOLwBLtbWjBoJIVobOKFanfjG/Xr8H/UA+umEVILPhMc2A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.9.1': - resolution: {integrity: sha512-c86g+67wTdI4TUCD7CaSd/13+oYuLQxVST4ZNJ5C+6i1kdnU3Us1L68N9MvbDLDQGJc9eo0pvuK6sCWkee+BzA==} + '@tauri-apps/cli-darwin-x64@2.9.2': + resolution: {integrity: sha512-nHHIY33noUmMOyFwAJz0xQyrYIXU+bae8MNos4TGsTo491YWAF2uzr6iW+Bq0N530xDcbe7EyRvDHgK43RmmVw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.1': - resolution: {integrity: sha512-IrB3gFQmueQKJjjisOcMktW/Gh6gxgqYO419doA3YZ7yIV5rbE8ZW52Q3I4AO+SlFEyVYer5kpi066p0JBlLGw==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.2': + resolution: {integrity: sha512-Dq17LBdSuzf+fWOKMIyiSao+Fcq4FiQwYYlx3Nk8oafDINc8sVBjC5gv2xp18KzYhk9teSWfmDpD1sj+D3t7uw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.9.1': - resolution: {integrity: sha512-Ke7TyXvu6HbWSkmVkFbbH19D3cLsd117YtXP/u9NIvSpYwKeFtnbpirrIUfPm44Q+PZFZ2Hvg8X9qoUiAK0zKw==} + '@tauri-apps/cli-linux-arm64-gnu@2.9.2': + resolution: {integrity: sha512-Pxj5k29Rxj9xEht4gdE744t5HLXTwBojkjYDXXyJ3mE+BEg9hFX5WkStg7OkyZwH60u8NSkDSMpo7MJTH9srmA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.9.1': - resolution: {integrity: sha512-sGvy75sv55oeMulR5ArwPD28DsDQxqTzLhXCrpU9/nbFg/JImmI7k994YE9fr3V0qE3Cjk5gjLldRNv7I9sjwQ==} + '@tauri-apps/cli-linux-arm64-musl@2.9.2': + resolution: {integrity: sha512-mx82BuD4q3Yj5Zw+LXveZgPaDCnmH2At2LosX1siK77kaD5Ap5FF+FN0V4y+3cwq+Hcrk9AhEUPbHqoNOx1R2g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.9.1': - resolution: {integrity: sha512-tEKbJydV3BdIxpAx8aGHW6VDg1xW4LlQuRD/QeFZdZNTreHJpMbJEcdvAcI+Hg6vgQpVpaoEldR9W4F6dYSLqQ==} + '@tauri-apps/cli-linux-riscv64-gnu@2.9.2': + resolution: {integrity: sha512-Ypm1nnr7k+ECC1+JfDcnxROHt6BX8t/4GplxBvdY68BDXtIcBbdhPWDos7MK+3bDmoaA0WSJbW+DUjpfSkyKgw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.9.1': - resolution: {integrity: sha512-mg5msXHagtHpyCVWgI01M26JeSrgE/otWyGdYcuTwyRYZYEJRTbcNt7hscOkdNlPBe7isScW7PVKbxmAjJJl4g==} + '@tauri-apps/cli-linux-x64-gnu@2.9.2': + resolution: {integrity: sha512-tg85cGIM9PWwsbQg8m3uah3SfoNapgUr4vhWtkqgeTDZOjQuQ2duTwCH4UiM7acBpbZHNzvRrxSFpv0U53TqQQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.9.1': - resolution: {integrity: sha512-lFZEXkpDreUe3zKilvnMsrnKP9gwQudaEjDnOz/GMzbzNceIuPfFZz0cR/ky1Aoq4eSvZonPKHhROq4owz4fzg==} + '@tauri-apps/cli-linux-x64-musl@2.9.2': + resolution: {integrity: sha512-xW8qaz9bcwR35W2gIg7fKG9e1Z34idOsGpD2zIPgxlJyF314B/1qie50hbOqt5AbbXHR4iRpxKE4kA2grqMmkg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.9.1': - resolution: {integrity: sha512-ejc5RAp/Lm1Aj0EQHaT+Wdt5PHfdgQV5hIDV00MV6HNbIb5W4ZUFxMDaRkAg65gl9MvY2fH396riePW3RoKXDw==} + '@tauri-apps/cli-win32-arm64-msvc@2.9.2': + resolution: {integrity: sha512-A1PshB8oHdY7zYOPlLD7Om7/aD9sOUVREd765ElIzYDtptWcALwOP9jb22Wi01vDTqxf98E4ZGIcG2gxr4FhiA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.9.1': - resolution: {integrity: sha512-fSATtJDc0fNjVB6ystyi8NbwhNFk8i8E05h6KrsC8Fio5eaJIJvPCbC9pdrPl6kkxN1X7fj25ErBbgfqgcK8Fg==} + '@tauri-apps/cli-win32-ia32-msvc@2.9.2': + resolution: {integrity: sha512-AuCi0Vnc4qkXRLCC58das0u45SmXAjqcOjqF324CBKa1Z7jjNJESm0Sc2oc2G2q6f2eAbAfi34s2iJNaJU1hlQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.9.1': - resolution: {integrity: sha512-/JHlOzpUDhjBOO9w167bcYxfJbcMQv7ykS/Y07xjtcga8np0rzUzVGWYmLMH7orKcDMC7wjhheEW1x8cbGma/Q==} + '@tauri-apps/cli-win32-x64-msvc@2.9.2': + resolution: {integrity: sha512-kDoejyfvME/mLkR4VofQnmVPTt/smJvoXuE3xgTbUwcUQKqawM8EyQvxOHQosaJYfQphHi7G0ya8UZo3PlDZig==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.9.1': - resolution: {integrity: sha512-kKi2/WWsNXKoMdatBl4xrT7e1Ce27JvsetBVfWuIb6D3ep/Y0WO5SIr70yarXOSWam8NyDur4ipzjZkg6m7VDg==} + '@tauri-apps/cli@2.9.2': + resolution: {integrity: sha512-aGzdVgxQW6WQ7e5nydPZ/30u8HvltHjO3Ytzf1wOxX1N5Yj2TsjKWRb/AWJlB95Huml3k3c/b6s0ijAvlSo9xw==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-clipboard-manager@2.3.1': - resolution: {integrity: sha512-lQMaUSFs5my8oEHuvQOLJoPHfxRJNn0gwXyTsyMhtQZBD4sYLiDux8p+EsagY6vv5SkSYWVViJYvlcPIjJ+Dog==} + '@tauri-apps/plugin-clipboard-manager@2.3.2': + resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} - '@tauri-apps/plugin-dialog@2.4.1': - resolution: {integrity: sha512-2eAueoxstrUnAf5cZWT9A/jzh4mTdUu646Q8zEX0a3RQmht7fQhdhxWfgQH4/of8iy1etDLKzokXbF2CxdBFHg==} + '@tauri-apps/plugin-dialog@2.4.2': + resolution: {integrity: sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==} - '@tauri-apps/plugin-fs@2.4.3': - resolution: {integrity: sha512-/ZVHrwf/FTMSReWgMfiraeJjBcIr4QrDQC5BalvSSLXgtEiELPjQkRaXox7zG7z5nB04m/TwTLqfeeBROg0LLQ==} + '@tauri-apps/plugin-fs@2.4.4': + resolution: {integrity: sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ==} - '@tauri-apps/plugin-http@2.5.3': - resolution: {integrity: sha512-YiizgUWd9jQBPGX2x3k1l6qdBiDYOo3FjqMEjkeJ0I8IRIkp6OQ8ff6fRcaiUt0sl8+h3r4983+6O/m+//PMVQ==} + '@tauri-apps/plugin-http@2.5.4': + resolution: {integrity: sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg==} - '@tauri-apps/plugin-process@2.3.0': - resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} - '@tauri-apps/plugin-shell@2.3.2': - resolution: {integrity: sha512-pop78bu3T25UVxL6kn/dFc+LZQhHB9WHCUoLIrXPagO4hlEGtdOKVEnIzQr4E9X8COrBAKcR/G/rNWuim8eEOg==} + '@tauri-apps/plugin-shell@2.3.3': + resolution: {integrity: sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==} '@tauri-apps/plugin-updater@2.9.0': resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} @@ -1712,18 +1772,6 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} - '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1766,8 +1814,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.9.1': - resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + '@types/node@24.9.2': + resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1958,17 +2006,17 @@ packages: terser: ^5.16.0 vite: ^7.0.0 - '@vitejs/plugin-react@5.1.0': - resolution: {integrity: sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==} + '@vitejs/plugin-react-swc@4.2.0': + resolution: {integrity: sha512-/tesahXD1qpkGC6FzMoFOJj0RyZdw9xLELOL+6jbElwmWfwOnIVy+IfpY+o9JfD9PKaR/Eyb6DNrvbXpuvA+8Q==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^4 || ^5 || ^6 || ^7 - '@vitest/expect@4.0.3': - resolution: {integrity: sha512-v3eSDx/bF25pzar6aEJrrdTXJduEBU3uSGXHslIdGIpJVP8tQQHV6x1ZfzbFQ/bLIomLSbR/2ZCfnaEGkWkiVQ==} + '@vitest/expect@4.0.6': + resolution: {integrity: sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==} - '@vitest/mocker@4.0.3': - resolution: {integrity: sha512-evZcRspIPbbiJEe748zI2BRu94ThCBE+RkjCpVF8yoVYuTV7hMe+4wLF/7K86r8GwJHSmAPnPbZhpXWWrg1qbA==} + '@vitest/mocker@4.0.6': + resolution: {integrity: sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -1978,20 +2026,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.0.3': - resolution: {integrity: sha512-N7gly/DRXzxa9w9sbDXwD9QNFYP2hw90LLLGDobPNwiWgyW95GMxsCt29/COIKKh3P7XJICR38PSDePenMBtsw==} + '@vitest/pretty-format@4.0.6': + resolution: {integrity: sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==} - '@vitest/runner@4.0.3': - resolution: {integrity: sha512-1/aK6fPM0lYXWyGKwop2Gbvz1plyTps/HDbIIJXYtJtspHjpXIeB3If07eWpVH4HW7Rmd3Rl+IS/+zEAXrRtXA==} + '@vitest/runner@4.0.6': + resolution: {integrity: sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==} - '@vitest/snapshot@4.0.3': - resolution: {integrity: sha512-amnYmvZ5MTjNCP1HZmdeczAPLRD6iOm9+2nMRUGxbe/6sQ0Ymur0NnR9LIrWS8JA3wKE71X25D6ya/3LN9YytA==} + '@vitest/snapshot@4.0.6': + resolution: {integrity: sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==} - '@vitest/spy@4.0.3': - resolution: {integrity: sha512-82vVL8Cqz7rbXaNUl35V2G7xeNMAjBdNOVaHbrzznT9BmiCiPOzhf0FhU3eP41nP1bLDm/5wWKZqkG4nyU95DQ==} + '@vitest/spy@4.0.6': + resolution: {integrity: sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==} - '@vitest/utils@4.0.3': - resolution: {integrity: sha512-qV6KJkq8W3piW6MDIbGOmn1xhvcW4DuA07alqaQ+vdx7YA49J85pnwnxigZVQFQw3tWnQNRKWwhz5wbP6iv/GQ==} + '@vitest/utils@4.0.6': + resolution: {integrity: sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2078,8 +2126,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.12.2: - resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + axios@1.13.1: + resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==} babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -2305,8 +2353,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.18: - resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -2548,15 +2596,15 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-dom@2.2.4: - resolution: {integrity: sha512-mrr56eZsNF0m6NrZxV3wojQnxADLqYGB0A5FHYRuMEX8jmkOy0Jb7v6B4IdzLt0kI1HhAhriOogxOkFlCch/4w==} + eslint-plugin-react-dom@2.3.1: + resolution: {integrity: sha512-Zuvb8iDYRbi8s7mYzvjHKD+i+loHjF6TKJiLGYM/t9F42OWU7V7b4sjIM7pXueukl0o8BSJXDVrQ+9sHOOmxBA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - eslint-plugin-react-hooks-extra@2.2.4: - resolution: {integrity: sha512-tS2xQyEx5QEbd71xkB0xo41/Vbnfo78eZRQYCMYkRmGzCb/Kkax6KXYs3wQH4HGWXBJ6KQloSPL+ISAhhqmtiQ==} + eslint-plugin-react-hooks-extra@2.3.1: + resolution: {integrity: sha512-2t4xQYhUEgPNq1SDQJEXuH3doT+h5spVmerX4rPnBFx0zG2sYfaJV1Gz6z40pI1L3CtBrZag5nFJ44AF/BEg0w==} engines: {node: '>=20.0.0'} peerDependencies: eslint: ^9.38.0 @@ -2568,8 +2616,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@2.2.4: - resolution: {integrity: sha512-/nUL8YeLI2g6pCK/HwNwH/KjlJ4x1kHErpOdMNVuOWEKxjaMSBFfb/gEaG6wrgNoCjQ9grzvtc4B3mkK9+fjDw==} + eslint-plugin-react-naming-convention@2.3.1: + resolution: {integrity: sha512-Ghh1o++3XDk3zNKF7DXy3kIN1kJYFiH7wvl4aJF5m9LytQGFrJKTA5kygAaWgR7iL8o4mjk5Ty6Be3OKskpHwA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 @@ -2580,15 +2628,15 @@ packages: peerDependencies: eslint: '>=8.40' - eslint-plugin-react-web-api@2.2.4: - resolution: {integrity: sha512-ZdGQkDBFp9wjnoqj3xPhIzfDv07wFMYdd+uwg94xl3Zya5G+d+oUduPvSu4Du+ei6hyeWngua0njUQ8BmppyjQ==} + eslint-plugin-react-web-api@2.3.1: + resolution: {integrity: sha512-rb7AYR9SCJkCDkFdqnD6JHNLKF1o29o6tZLSaPdzA1Ssxh7/VKgJ8GpTrgl3Rv+Gnyn+w+3w4XE14d7T1Db9nA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - eslint-plugin-react-x@2.2.4: - resolution: {integrity: sha512-mdoxE1SPt653/udAGQvwPob7ZgaPjIk47G0MWNwKtzLyuI0oD5X+6uq1QAn99TOM1q+sqjsCvvL7aMwGDuZ6aw==} + eslint-plugin-react-x@2.3.1: + resolution: {integrity: sha512-7zfi297NfkoEtqaz2W953gdK4J9nJD5okVhJVxgrcrP+9FVertkGqpbWtMZLpQuWJ216FncY8P6t1U+af8KNOA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 @@ -2615,8 +2663,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.38.0: - resolution: {integrity: sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==} + eslint@9.39.0: + resolution: {integrity: sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2816,8 +2864,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} globalthis@1.0.4: @@ -3563,14 +3611,14 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.65.0: - resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==} + react-hook-form@7.66.0: + resolution: {integrity: sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.2.1: - resolution: {integrity: sha512-z7TVwd8q4AjFo2n7oOwzNusY7xVL4uHykwX1zZRvasUQnmnXlp7Z1FZqXvhK/6hQaCvWTZmZW1bMaUWKowtvVw==} + react-i18next@16.2.3: + resolution: {integrity: sha512-O0t2zvmIz7nHWKNfIL+O/NTIbpTaOPY0vZov779hegbep3IZ+xcqkeVPKWBSXwzdkiv77q8zmq9toKIUys1x3A==} peerDependencies: i18next: '>= 25.5.2' react: '>= 16.8.0' @@ -3604,12 +3652,8 @@ packages: react: '>=16.8.0 <20.0.0' react-dom: '>=16.8.0 <20.0.0' - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - - react-router@7.9.4: - resolution: {integrity: sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==} + react-router@7.9.5: + resolution: {integrity: sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3722,8 +3766,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass@1.93.2: - resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} + sass@1.93.3: + resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} engines: {node: '>=14.0.0'} hasBin: true @@ -3917,8 +3961,8 @@ packages: systemjs@6.15.1: resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==} - tar@7.5.1: - resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c: @@ -4091,8 +4135,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-plugin-monaco-editor@1.1.0: - resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==} + vite-plugin-monaco-editor-esm@2.0.2: + resolution: {integrity: sha512-XVkOpL/r0rw1NpbO30vUwG4S0THkC9KB1vjjV8olGd49h4/EQsKl3DrxB6KRDwyZNC9mKiiZgk2L6njUYj3oKQ==} peerDependencies: monaco-editor: '>=0.33.0' @@ -4141,18 +4185,18 @@ packages: yaml: optional: true - vitest@4.0.3: - resolution: {integrity: sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA==} + vitest@4.0.6: + resolution: {integrity: sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.3 - '@vitest/browser-preview': 4.0.3 - '@vitest/browser-webdriverio': 4.0.3 - '@vitest/ui': 4.0.3 + '@vitest/browser-playwright': 4.0.6 + '@vitest/browser-preview': 4.0.6 + '@vitest/browser-webdriverio': 4.0.6 + '@vitest/ui': 4.0.6 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -4273,24 +4317,6 @@ packages: zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} - zustand@5.0.8: - resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4790,16 +4816,6 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -5176,34 +5192,34 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.8.0(eslint@9.38.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.8.0(eslint@9.39.0(jiti@2.6.1))': dependencies: - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.2.4 + '@eslint-react/eff': 2.3.1 '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) string-ts: 2.2.1 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/core@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/core@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) birecord: 0.1.1 ts-pattern: 5.9.0 transitivePeerDependencies: @@ -5211,31 +5227,31 @@ snapshots: - supports-color - typescript - '@eslint-react/eff@2.2.4': {} + '@eslint-react/eff@2.3.1': {} - '@eslint-react/eslint-plugin@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) - eslint-plugin-react-dom: 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) + eslint-plugin-react-dom: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.2.4 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 zod: 4.1.12 transitivePeerDependencies: @@ -5243,13 +5259,13 @@ snapshots: - supports-color - typescript - '@eslint-react/var@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 transitivePeerDependencies: - eslint @@ -5264,11 +5280,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.1': + '@eslint/config-helpers@0.4.2': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 - '@eslint/core@0.16.0': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -5286,13 +5302,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.38.0': {} + '@eslint/js@9.39.0': {} '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.4.0': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 levn: 0.4.1 '@fastify/busboy@2.1.1': {} @@ -5772,76 +5788,128 @@ snapshots: transitivePeerDependencies: - supports-color + '@swc/core-darwin-arm64@1.14.0': + optional: true + + '@swc/core-darwin-x64@1.14.0': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.14.0': + optional: true + + '@swc/core-linux-arm64-gnu@1.14.0': + optional: true + + '@swc/core-linux-arm64-musl@1.14.0': + optional: true + + '@swc/core-linux-x64-gnu@1.14.0': + optional: true + + '@swc/core-linux-x64-musl@1.14.0': + optional: true + + '@swc/core-win32-arm64-msvc@1.14.0': + optional: true + + '@swc/core-win32-ia32-msvc@1.14.0': + optional: true + + '@swc/core-win32-x64-msvc@1.14.0': + optional: true + + '@swc/core@1.14.0': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.14.0 + '@swc/core-darwin-x64': 1.14.0 + '@swc/core-linux-arm-gnueabihf': 1.14.0 + '@swc/core-linux-arm64-gnu': 1.14.0 + '@swc/core-linux-arm64-musl': 1.14.0 + '@swc/core-linux-x64-gnu': 1.14.0 + '@swc/core-linux-x64-musl': 1.14.0 + '@swc/core-win32-arm64-msvc': 1.14.0 + '@swc/core-win32-ia32-msvc': 1.14.0 + '@swc/core-win32-x64-msvc': 1.14.0 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + '@tauri-apps/api@2.9.0': {} - '@tauri-apps/cli-darwin-arm64@2.9.1': + '@tauri-apps/cli-darwin-arm64@2.9.2': optional: true - '@tauri-apps/cli-darwin-x64@2.9.1': + '@tauri-apps/cli-darwin-x64@2.9.2': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.1': + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.2': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.9.1': + '@tauri-apps/cli-linux-arm64-gnu@2.9.2': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.9.1': + '@tauri-apps/cli-linux-arm64-musl@2.9.2': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.9.1': + '@tauri-apps/cli-linux-riscv64-gnu@2.9.2': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.9.1': + '@tauri-apps/cli-linux-x64-gnu@2.9.2': optional: true - '@tauri-apps/cli-linux-x64-musl@2.9.1': + '@tauri-apps/cli-linux-x64-musl@2.9.2': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.9.1': + '@tauri-apps/cli-win32-arm64-msvc@2.9.2': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.9.1': + '@tauri-apps/cli-win32-ia32-msvc@2.9.2': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.9.1': + '@tauri-apps/cli-win32-x64-msvc@2.9.2': optional: true - '@tauri-apps/cli@2.9.1': + '@tauri-apps/cli@2.9.2': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.9.1 - '@tauri-apps/cli-darwin-x64': 2.9.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.1 - '@tauri-apps/cli-linux-arm64-gnu': 2.9.1 - '@tauri-apps/cli-linux-arm64-musl': 2.9.1 - '@tauri-apps/cli-linux-riscv64-gnu': 2.9.1 - '@tauri-apps/cli-linux-x64-gnu': 2.9.1 - '@tauri-apps/cli-linux-x64-musl': 2.9.1 - '@tauri-apps/cli-win32-arm64-msvc': 2.9.1 - '@tauri-apps/cli-win32-ia32-msvc': 2.9.1 - '@tauri-apps/cli-win32-x64-msvc': 2.9.1 + '@tauri-apps/cli-darwin-arm64': 2.9.2 + '@tauri-apps/cli-darwin-x64': 2.9.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.2 + '@tauri-apps/cli-linux-arm64-musl': 2.9.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.2 + '@tauri-apps/cli-linux-x64-gnu': 2.9.2 + '@tauri-apps/cli-linux-x64-musl': 2.9.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.2 + '@tauri-apps/cli-win32-x64-msvc': 2.9.2 - '@tauri-apps/plugin-clipboard-manager@2.3.1': + '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: '@tauri-apps/api': 2.9.0 - '@tauri-apps/plugin-dialog@2.4.1': + '@tauri-apps/plugin-dialog@2.4.2': dependencies: '@tauri-apps/api': 2.9.0 - '@tauri-apps/plugin-fs@2.4.3': + '@tauri-apps/plugin-fs@2.4.4': dependencies: '@tauri-apps/api': 2.9.0 - '@tauri-apps/plugin-http@2.5.3': + '@tauri-apps/plugin-http@2.5.4': dependencies: '@tauri-apps/api': 2.9.0 - '@tauri-apps/plugin-process@2.3.0': + '@tauri-apps/plugin-process@2.3.1': dependencies: '@tauri-apps/api': 2.9.0 - '@tauri-apps/plugin-shell@2.3.2': + '@tauri-apps/plugin-shell@2.3.3': dependencies: '@tauri-apps/api': 2.9.0 @@ -5854,27 +5922,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.28.4 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - - '@types/babel__traverse@7.20.7': - dependencies: - '@babel/types': 7.28.4 - '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -5916,7 +5963,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.9.1': + '@types/node@24.9.2': dependencies: undici-types: 7.16.0 @@ -5940,15 +5987,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.2 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -5957,14 +6004,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.2 debug: 4.4.3 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5987,13 +6034,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -6017,13 +6064,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.38.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6094,7 +6141,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) @@ -6109,59 +6156,55 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.0 - vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) '@rolldown/pluginutils': 1.0.0-beta.43 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + '@swc/core': 1.14.0 + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - - supports-color + - '@swc/helpers' - '@vitest/expect@4.0.3': + '@vitest/expect@4.0.6': dependencies: '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.0.3 - '@vitest/utils': 4.0.3 + '@vitest/spy': 4.0.6 + '@vitest/utils': 4.0.6 chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.3(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@vitest/spy': 4.0.3 + '@vitest/spy': 4.0.6 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) - '@vitest/pretty-format@4.0.3': + '@vitest/pretty-format@4.0.6': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.3': + '@vitest/runner@4.0.6': dependencies: - '@vitest/utils': 4.0.3 + '@vitest/utils': 4.0.6 pathe: 2.0.3 - '@vitest/snapshot@4.0.3': + '@vitest/snapshot@4.0.6': dependencies: - '@vitest/pretty-format': 4.0.3 + '@vitest/pretty-format': 4.0.6 magic-string: 0.30.19 pathe: 2.0.3 - '@vitest/spy@4.0.3': {} + '@vitest/spy@4.0.6': {} - '@vitest/utils@4.0.3': + '@vitest/utils@4.0.6': dependencies: - '@vitest/pretty-format': 4.0.3 + '@vitest/pretty-format': 4.0.6 tinyrainbow: 3.0.3 acorn-jsx@5.3.2(acorn@8.15.0): @@ -6178,7 +6221,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 '@types/js-cookie': 3.0.6 - dayjs: 1.11.18 + dayjs: 1.11.19 intersection-observer: 0.12.2 js-cookie: 3.0.5 lodash: 4.17.21 @@ -6278,7 +6321,7 @@ snapshots: possible-typed-array-names: 1.1.0 optional: true - axios@1.12.2: + axios@1.13.1: dependencies: follow-redirects: 1.15.9 form-data: 4.0.4 @@ -6514,7 +6557,7 @@ snapshots: is-data-view: 1.0.2 optional: true - dayjs@1.11.18: {} + dayjs@1.11.19: {} debug@3.2.7: dependencies: @@ -6743,9 +6786,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)): dependencies: - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -6763,10 +6806,10 @@ snapshots: - supports-color optional: true - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.38.0(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 @@ -6774,29 +6817,29 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.38.0(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.0(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.38.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.38.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.6.1)): dependencies: '@typescript-eslint/types': 8.46.2 comment-parser: 1.4.1 debug: 4.4.3 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.0.3 @@ -6804,12 +6847,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.38.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6818,9 +6861,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.38.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6832,122 +6875,122 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color optional: true - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@2.6.1)))(eslint@9.39.0(jiti@2.6.1))(prettier@3.6.2): dependencies: - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) prettier: 3.6.2 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@9.38.0(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@9.39.0(jiti@2.6.1)) - eslint-plugin-react-dom@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@9.38.0(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.0(jiti@2.6.1)): dependencies: '@babel/core': 7.28.4 '@babel/parser': 7.28.4 - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.1.12 zod-validation-error: 4.0.2(zod@4.1.12) transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.24(eslint@9.38.0(jiti@2.6.1)): + eslint-plugin-react-refresh@0.4.24(eslint@9.39.0(jiti@2.6.1)): dependencies: - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) - eslint-plugin-react-web-api@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.2.4 - '@eslint-react/shared': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.2.4(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.1 + '@eslint-react/shared': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 - eslint: 9.38.0(jiti@2.6.1) - is-immutable-type: 5.0.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) + is-immutable-type: 5.0.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) string-ts: 2.2.1 ts-api-utils: 2.1.0(typescript@5.9.3) ts-pattern: 5.9.0 @@ -6955,11 +6998,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1)): dependencies: - eslint: 9.38.0(jiti@2.6.1) + eslint: 9.39.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -6970,16 +7013,16 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.38.0(jiti@2.6.1): + eslint@9.39.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.38.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.1 - '@eslint/core': 0.16.0 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.38.0 - '@eslint/plugin-kit': 0.4.0 + '@eslint/js': 9.39.0 + '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -7211,7 +7254,7 @@ snapshots: globals@14.0.0: {} - globals@16.4.0: {} + globals@16.5.0: {} globalthis@1.0.4: dependencies: @@ -7416,10 +7459,10 @@ snapshots: is-hexadecimal@2.0.1: {} - is-immutable-type@5.0.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + is-immutable-type@5.0.1(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) ts-declaration-location: 1.0.7(typescript@5.9.3) typescript: 5.9.3 @@ -8117,11 +8160,11 @@ snapshots: react-fast-compare@3.2.2: {} - react-hook-form@7.65.0(react@19.2.0): + react-hook-form@7.66.0(react@19.2.0): dependencies: react: 19.2.0 - react-i18next@16.2.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 @@ -8160,9 +8203,7 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - react-refresh@0.18.0: {} - - react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.0.2 react: 19.2.0 @@ -8325,7 +8366,7 @@ snapshots: is-regex: 1.2.1 optional: true - sass@1.93.2: + sass@1.93.3: dependencies: chokidar: 4.0.3 immutable: 5.1.2 @@ -8548,7 +8589,7 @@ snapshots: systemjs@6.15.1: {} - tar@7.5.1: + tar@7.5.2: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -8659,13 +8700,13 @@ snapshots: types-pac@1.0.3: {} - typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.38.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8780,22 +8821,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-monaco-editor@1.1.0(monaco-editor@0.54.0): + vite-plugin-monaco-editor-esm@2.0.2(monaco-editor@0.54.0): dependencies: monaco-editor: 0.54.0 - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.46.2) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): + vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): dependencies: esbuild: 0.25.4 fdir: 6.5.0(picomatch@4.0.3) @@ -8804,22 +8845,22 @@ snapshots: rollup: 4.46.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.9.1 + '@types/node': 24.9.2 fsevents: 2.3.3 jiti: 2.6.1 - sass: 1.93.2 + sass: 1.93.3 terser: 5.44.0 yaml: 2.8.1 - vitest@4.0.3(@types/debug@4.1.12)(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): + vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1): dependencies: - '@vitest/expect': 4.0.3 - '@vitest/mocker': 4.0.3(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.3 - '@vitest/runner': 4.0.3 - '@vitest/snapshot': 4.0.3 - '@vitest/spy': 4.0.3 - '@vitest/utils': 4.0.3 + '@vitest/expect': 4.0.6 + '@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.6 + '@vitest/runner': 4.0.6 + '@vitest/snapshot': 4.0.6 + '@vitest/spy': 4.0.6 + '@vitest/utils': 4.0.6 debug: 4.4.3 es-module-lexer: 1.7.0 expect-type: 1.2.2 @@ -8831,11 +8872,11 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.9.1 + '@types/node': 24.9.2 transitivePeerDependencies: - jiti - less @@ -8959,10 +9000,4 @@ snapshots: zod@4.1.12: {} - zustand@5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): - optionalDependencies: - '@types/react': 19.2.2 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) - zwitch@2.0.4: {} diff --git a/renovate.json b/renovate.json index e224fded..2801483a 100644 --- a/renovate.json +++ b/renovate.json @@ -46,6 +46,6 @@ "matchCurrentVersion": "<8.0.0" } ], - "postUpdateOptions": ["pnpmDedupe"], + "postUpdateOptions": ["pnpmDedupe", "updateCargoLock"], "ignoreDeps": ["criterion"] } diff --git a/scripts/prebuild.mjs b/scripts/prebuild.mjs index 1cf801c5..d127a0ca 100644 --- a/scripts/prebuild.mjs +++ b/scripts/prebuild.mjs @@ -18,7 +18,6 @@ import { log_debug, log_error, log_info, log_success } from "./utils.mjs"; * 3. Use file hash to detect changes and skip unnecessary chmod/copy operations * 4. Use --force or -f flag to force re-download and update all resources * - * This optimization significantly reduces build time for local development */ const cwd = process.cwd(); @@ -56,8 +55,7 @@ const ARCH_MAP = { const arg1 = process.argv.slice(2)[0]; const arg2 = process.argv.slice(2)[1]; -let target; -target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1; +let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1; const { platform, arch } = target ? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] } : process; @@ -68,7 +66,9 @@ const SIDECAR_HOST = target .toString() .match(/(?<=host: ).+(?=\s*)/g)[0]; -/* ======= Version Cache Functions ======= */ +// ======================= +// Version Cache +// ======================= async function loadVersionCache() { try { if (fs.existsSync(VERSION_CACHE_FILE)) { @@ -80,7 +80,6 @@ async function loadVersionCache() { } return {}; } - async function saveVersionCache(cache) { try { await fsp.mkdir(TEMP_DIR, { recursive: true }); @@ -90,28 +89,24 @@ async function saveVersionCache(cache) { log_debug("Failed to save version cache:", err.message); } } - async function getCachedVersion(key) { const cache = await loadVersionCache(); const cached = cache[key]; if (cached && Date.now() - cached.timestamp < 3600000) { - // 1小时内有效 log_info(`Using cached version for ${key}: ${cached.version}`); return cached.version; } return null; } - async function setCachedVersion(key, version) { const cache = await loadVersionCache(); - cache[key] = { - version, - timestamp: Date.now(), - }; + cache[key] = { version, timestamp: Date.now() }; await saveVersionCache(cache); } -/* ======= File Hash Functions ======= */ +// ======================= +// Hash Cache & File Hash +// ======================= async function calculateFileHash(filePath) { try { const fileBuffer = await fsp.readFile(filePath); @@ -122,7 +117,6 @@ async function calculateFileHash(filePath) { return null; } } - async function loadHashCache() { try { if (fs.existsSync(HASH_CACHE_FILE)) { @@ -134,7 +128,6 @@ async function loadHashCache() { } return {}; } - async function saveHashCache(cache) { try { await fsp.mkdir(TEMP_DIR, { recursive: true }); @@ -144,28 +137,20 @@ async function saveHashCache(cache) { log_debug("Failed to save hash cache:", err.message); } } - async function hasFileChanged(filePath, targetPath) { if (FORCE) return true; if (!fs.existsSync(targetPath)) return true; - const hashCache = await loadHashCache(); const sourceHash = await calculateFileHash(filePath); const targetHash = await calculateFileHash(targetPath); - if (!sourceHash || !targetHash) return true; - const cacheKey = targetPath; const cachedHash = hashCache[cacheKey]; - if (cachedHash === sourceHash && sourceHash === targetHash) { - // 文件未变化,不输出日志 return false; } - return true; } - async function updateHashCache(targetPath) { const hashCache = await loadHashCache(); const hash = await calculateFileHash(targetPath); @@ -175,18 +160,25 @@ async function updateHashCache(targetPath) { } } -/* ======= clash meta alpha======= */ +// ======================= +// Meta maps (stable & alpha) +// ======================= const META_ALPHA_VERSION_URL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt"; const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`; let META_ALPHA_VERSION; +const META_VERSION_URL = + "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt"; +const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`; +let META_VERSION; + const META_ALPHA_MAP = { "win32-x64": "mihomo-windows-amd64-v2", "win32-ia32": "mihomo-windows-386", "win32-arm64": "mihomo-windows-arm64", - "darwin-x64": "mihomo-darwin-amd64-v1", - "darwin-arm64": "mihomo-darwin-arm64", + "darwin-x64": "mihomo-darwin-amd64-v1-go122", + "darwin-arm64": "mihomo-darwin-arm64-go122", "linux-x64": "mihomo-linux-amd64-v2", "linux-ia32": "mihomo-linux-386", "linux-arm64": "mihomo-linux-arm64", @@ -195,9 +187,24 @@ const META_ALPHA_MAP = { "linux-loong64": "mihomo-linux-loong64", }; -// Fetch the latest alpha release version from the version.txt file +const META_MAP = { + "win32-x64": "mihomo-windows-amd64-v2", + "win32-ia32": "mihomo-windows-386", + "win32-arm64": "mihomo-windows-arm64", + "darwin-x64": "mihomo-darwin-amd64-v2-go122", + "darwin-arm64": "mihomo-darwin-arm64-go122", + "linux-x64": "mihomo-linux-amd64-v2", + "linux-ia32": "mihomo-linux-386", + "linux-arm64": "mihomo-linux-arm64", + "linux-arm": "mihomo-linux-armv7", + "linux-riscv64": "mihomo-linux-riscv64", + "linux-loong64": "mihomo-linux-loong64", +}; + +// ======================= +// Fetch latest versions +// ======================= async function getLatestAlphaVersion() { - // 如果不强制更新,先尝试从缓存获取 if (!FORCE) { const cached = await getCachedVersion("META_ALPHA_VERSION"); if (cached) { @@ -205,58 +212,33 @@ async function getLatestAlphaVersion() { return; } } - const options = {}; - const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy || process.env.HTTPS_PROXY || process.env.https_proxy; + if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy); - if (httpProxy) { - options.agent = new HttpsProxyAgent(httpProxy); - } try { const response = await fetch(META_ALPHA_VERSION_URL, { ...options, method: "GET", }); - let v = await response.text(); - META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces + if (!response.ok) + throw new Error( + `Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`, + ); + META_ALPHA_VERSION = (await response.text()).trim(); log_info(`Latest alpha version: ${META_ALPHA_VERSION}`); - - // 保存到缓存 await setCachedVersion("META_ALPHA_VERSION", META_ALPHA_VERSION); - } catch (error) { - log_error("Error fetching latest alpha version:", error.message); + } catch (err) { + log_error("Error fetching latest alpha version:", err.message); process.exit(1); } } -/* ======= clash meta stable ======= */ -const META_VERSION_URL = - "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt"; -const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`; -let META_VERSION; - -const META_MAP = { - "win32-x64": "mihomo-windows-amd64-v2", - "win32-ia32": "mihomo-windows-386", - "win32-arm64": "mihomo-windows-arm64", - "darwin-x64": "mihomo-darwin-amd64-v2", - "darwin-arm64": "mihomo-darwin-arm64", - "linux-x64": "mihomo-linux-amd64-v2", - "linux-ia32": "mihomo-linux-386", - "linux-arm64": "mihomo-linux-arm64", - "linux-arm": "mihomo-linux-armv7", - "linux-riscv64": "mihomo-linux-riscv64", - "linux-loong64": "mihomo-linux-loong64", -}; - -// Fetch the latest release version from the version.txt file async function getLatestReleaseVersion() { - // 如果不强制更新,先尝试从缓存获取 if (!FORCE) { const cached = await getCachedVersion("META_VERSION"); if (cached) { @@ -264,67 +246,57 @@ async function getLatestReleaseVersion() { return; } } - const options = {}; - const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy || process.env.HTTPS_PROXY || process.env.https_proxy; + if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy); - if (httpProxy) { - options.agent = new HttpsProxyAgent(httpProxy); - } try { const response = await fetch(META_VERSION_URL, { ...options, method: "GET", }); - let v = await response.text(); - META_VERSION = v.trim(); // Trim to remove extra whitespaces + if (!response.ok) + throw new Error( + `Failed to fetch ${META_VERSION_URL}: ${response.status}`, + ); + META_VERSION = (await response.text()).trim(); log_info(`Latest release version: ${META_VERSION}`); - - // 保存到缓存 await setCachedVersion("META_VERSION", META_VERSION); - } catch (error) { - log_error("Error fetching latest release version:", error.message); + } catch (err) { + log_error("Error fetching latest release version:", err.message); process.exit(1); } } -/* - * check available - */ +// ======================= +// Validate availability +// ======================= if (!META_MAP[`${platform}-${arch}`]) { - throw new Error( - `clash meta alpha unsupported platform "${platform}-${arch}"`, - ); + throw new Error(`clash meta unsupported platform "${platform}-${arch}"`); } - if (!META_ALPHA_MAP[`${platform}-${arch}`]) { throw new Error( `clash meta alpha unsupported platform "${platform}-${arch}"`, ); } -/** - * core info - */ +// ======================= +// Build meta objects +// ======================= function clashMetaAlpha() { const name = META_ALPHA_MAP[`${platform}-${arch}`]; const isWin = platform === "win32"; const urlExt = isWin ? "zip" : "gz"; - const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`; - const exeFile = `${name}${isWin ? ".exe" : ""}`; - const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`; - return { name: "verge-mihomo-alpha", targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, - exeFile, - zipFile, - downloadURL, + exeFile: `${name}${isWin ? ".exe" : ""}`, + zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`, + downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`, }; } @@ -332,40 +304,83 @@ function clashMeta() { const name = META_MAP[`${platform}-${arch}`]; const isWin = platform === "win32"; const urlExt = isWin ? "zip" : "gz"; - const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`; - const exeFile = `${name}${isWin ? ".exe" : ""}`; - const zipFile = `${name}-${META_VERSION}.${urlExt}`; - return { name: "verge-mihomo", targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`, - exeFile, - zipFile, - downloadURL, + exeFile: `${name}${isWin ? ".exe" : ""}`, + zipFile: `${name}-${META_VERSION}.${urlExt}`, + downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`, }; } -/** - * download sidecar and rename - */ + +// ======================= +// download helper (增强:status + magic bytes) +// ======================= +async function downloadFile(url, outPath) { + const options = {}; + const httpProxy = + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.HTTPS_PROXY || + process.env.https_proxy; + if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy); + + const response = await fetch(url, { + ...options, + method: "GET", + headers: { "Content-Type": "application/octet-stream" }, + }); + if (!response.ok) { + const body = await response.text().catch(() => ""); + // 将 body 写到文件以便排查(可通过临时目录查看) + await fsp.mkdir(path.dirname(outPath), { recursive: true }); + await fsp.writeFile(outPath, body); + throw new Error(`Failed to download ${url}: status ${response.status}`); + } + + const buf = Buffer.from(await response.arrayBuffer()); + await fsp.mkdir(path.dirname(outPath), { recursive: true }); + + // 简单 magic 字节检查 + if (url.endsWith(".gz") || url.endsWith(".tgz")) { + if (!(buf[0] === 0x1f && buf[1] === 0x8b)) { + await fsp.writeFile(outPath, buf); + throw new Error( + `Downloaded file for ${url} is not a valid gzip (magic mismatch).`, + ); + } + } else if (url.endsWith(".zip")) { + if (!(buf[0] === 0x50 && buf[1] === 0x4b)) { + await fsp.writeFile(outPath, buf); + throw new Error( + `Downloaded file for ${url} is not a valid zip (magic mismatch).`, + ); + } + } + + await fsp.writeFile(outPath, buf); + log_success(`download finished: ${url}`); +} + +// ======================= +// resolveSidecar (支持 zip / tgz / gz) +// ======================= async function resolveSidecar(binInfo) { const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo; - const sidecarDir = path.join(cwd, "src-tauri", "sidecar"); const sidecarPath = path.join(sidecarDir, targetFile); - await fsp.mkdir(sidecarDir, { recursive: true }); - // 检查文件是否已存在,如果存在则跳过重复下载 if (!FORCE && fs.existsSync(sidecarPath)) { - log_success(`"${name}" already exists, skipping download to save time`); + log_success(`"${name}" already exists, skipping download`); return; } const tempDir = path.join(TEMP_DIR, name); const tempZip = path.join(tempDir, zipFile); const tempExe = path.join(tempDir, exeFile); - await fsp.mkdir(tempDir, { recursive: true }); + try { if (!fs.existsSync(tempZip)) { await downloadFile(downloadURL, tempZip); @@ -374,78 +389,76 @@ async function resolveSidecar(binInfo) { if (zipFile.endsWith(".zip")) { const zip = new AdmZip(tempZip); zip.getEntries().forEach((entry) => { - log_debug(`"${name}" entry name`, entry.entryName); + log_debug(`"${name}" entry: ${entry.entryName}`); }); zip.extractAllTo(tempDir, true); - await fsp.rename(tempExe, sidecarPath); + // 尝试按 exeFile 重命名,否则找第一个可执行文件 + if (fs.existsSync(tempExe)) { + await fsp.rename(tempExe, sidecarPath); + } else { + // 搜索候选 + const files = await fsp.readdir(tempDir); + const candidate = files.find( + (f) => + f === path.basename(exeFile) || + f.endsWith(".exe") || + !f.includes("."), + ); + if (!candidate) + throw new Error(`Expected binary not found in ${tempDir}`); + await fsp.rename(path.join(tempDir, candidate), sidecarPath); + } + if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`); log_success(`unzip finished: "${name}"`); } else if (zipFile.endsWith(".tgz")) { - // tgz - await fsp.mkdir(tempDir, { recursive: true }); - await extract({ - cwd: tempDir, - file: tempZip, - //strip: 1, // 可能需要根据实际的 .tgz 文件结构调整 - }); + await extract({ cwd: tempDir, file: tempZip }); const files = await fsp.readdir(tempDir); - log_debug(`"${name}" files in tempDir:`, files); - const extractedFile = files.find((file) => file.startsWith("虚空终端-")); - if (extractedFile) { - const extractedFilePath = path.join(tempDir, extractedFile); - await fsp.rename(extractedFilePath, sidecarPath); - log_success(`"${name}" file renamed to "${sidecarPath}"`); - execSync(`chmod 755 ${sidecarPath}`); - log_success(`chmod binary finished: "${name}"`); - } else { - throw new Error(`Expected file not found in ${tempDir}`); - } + log_debug(`"${name}" extracted files:`, files); + // 优先寻找给定 exeFile 或已知前缀 + let extracted = files.find( + (f) => + f === path.basename(exeFile) || + f.startsWith("虚空终端-") || + !f.includes("."), + ); + if (!extracted) extracted = files[0]; + if (!extracted) throw new Error(`Expected file not found in ${tempDir}`); + await fsp.rename(path.join(tempDir, extracted), sidecarPath); + execSync(`chmod 755 ${sidecarPath}`); + log_success(`tgz processed: "${name}"`); } else { - // gz + // .gz const readStream = fs.createReadStream(tempZip); const writeStream = fs.createWriteStream(sidecarPath); await new Promise((resolve, reject) => { - const onError = (error) => { - log_error(`"${name}" gz failed:`, error.message); - reject(error); - }; readStream - .pipe(zlib.createGunzip().on("error", onError)) + .pipe(zlib.createGunzip()) + .on("error", (e) => { + log_error(`gunzip error for ${name}:`, e.message); + reject(e); + }) .pipe(writeStream) .on("finish", () => { - execSync(`chmod 755 ${sidecarPath}`); - log_success(`chmod binary finished: "${name}"`); + if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`); resolve(); }) - .on("error", onError); + .on("error", (e) => { + log_error(`write stream error for ${name}:`, e.message); + reject(e); + }); }); + log_success(`gz binary processed: "${name}"`); } } catch (err) { - // 需要删除文件 await fsp.rm(sidecarPath, { recursive: true, force: true }); throw err; } finally { - // delete temp dir await fsp.rm(tempDir, { recursive: true, force: true }); } } -const resolveSetDnsScript = () => - resolveResource({ - file: "set_dns.sh", - localPath: path.join(cwd, "scripts/set_dns.sh"), - }); -const resolveUnSetDnsScript = () => - resolveResource({ - file: "unset_dns.sh", - localPath: path.join(cwd, "scripts/unset_dns.sh"), - }); - -/** - * download the file to the resources dir - */ async function resolveResource(binInfo) { const { file, downloadURL, localPath } = binInfo; - const resDir = path.join(cwd, "src-tauri/resources"); const targetPath = path.join(resDir, file); @@ -465,12 +478,9 @@ async function resolveResource(binInfo) { } if (localPath) { - // 检查文件哈希是否变化 if (!(await hasFileChanged(localPath, targetPath))) { - // 文件未变化,静默跳过 return; } - await fsp.mkdir(resDir, { recursive: true }); await fsp.copyFile(localPath, targetPath); await updateHashCache(targetPath); @@ -480,44 +490,17 @@ async function resolveResource(binInfo) { log_success(`${file} finished`); } -/** - * download file and save to `path` - */ async function downloadFile(url, path) { - const options = {}; - - const httpProxy = - process.env.HTTP_PROXY || - process.env.http_proxy || - process.env.HTTPS_PROXY || - process.env.https_proxy; - - if (httpProxy) { - options.agent = new HttpsProxyAgent(httpProxy); - } - - const response = await fetch(url, { - ...options, - method: "GET", - headers: { "Content-Type": "application/octet-stream" }, - }); - const buffer = await response.arrayBuffer(); - await fsp.writeFile(path, new Uint8Array(buffer)); - - log_success(`download finished: ${url}`); -} - -// SimpleSC.dll +// SimpleSC.dll (win plugin) const resolvePlugin = async () => { const url = "https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip"; - const tempDir = path.join(TEMP_DIR, "SimpleSC"); const tempZip = path.join( tempDir, "NSIS_Simple_Service_Plugin_Unicode_1.30.zip", ); const tempDll = path.join(tempDir, "SimpleSC.dll"); - const pluginDir = path.join(process.env.APPDATA, "Local/NSIS"); + const pluginDir = path.join(process.env.APPDATA || "", "Local/NSIS"); const pluginPath = path.join(pluginDir, "SimpleSC.dll"); await fsp.mkdir(pluginDir, { recursive: true }); await fsp.mkdir(tempDir, { recursive: true }); @@ -527,18 +510,33 @@ const resolvePlugin = async () => { await downloadFile(url, tempZip); } const zip = new AdmZip(tempZip); - zip.getEntries().forEach((entry) => { - log_debug(`"SimpleSC" entry name`, entry.entryName); - }); + zip + .getEntries() + .forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName)); zip.extractAllTo(tempDir, true); - await fsp.cp(tempDll, pluginPath, { recursive: true, force: true }); - log_success(`unzip finished: "SimpleSC"`); + if (fs.existsSync(tempDll)) { + await fsp.cp(tempDll, pluginPath, { recursive: true, force: true }); + log_success(`unzip finished: "SimpleSC"`); + } else { + // 如果 dll 名称不同,尝试找到 dll + const files = await fsp.readdir(tempDir); + const dll = files.find((f) => f.toLowerCase().endsWith(".dll")); + if (dll) { + await fsp.cp(path.join(tempDir, dll), pluginPath, { + recursive: true, + force: true, + }); + log_success(`unzip finished: "SimpleSC" (found ${dll})`); + } else { + throw new Error("SimpleSC.dll not found in zip"); + } + } } finally { await fsp.rm(tempDir, { recursive: true, force: true }); } }; -// service chmod +// service chmod (保留并使用 glob) const resolveServicePermission = async () => { const serviceExecutables = [ "clash-verge-service*", @@ -550,23 +548,20 @@ const resolveServicePermission = async () => { let hasChanges = false; for (let f of serviceExecutables) { - // 使用glob模块来处理通配符 const files = glob.sync(path.join(resDir, f)); for (let filePath of files) { if (fs.existsSync(filePath)) { const currentHash = await calculateFileHash(filePath); const cacheKey = `${filePath}_chmod`; - - // 检查文件哈希是否变化 if (!FORCE && hashCache[cacheKey] === currentHash) { - // 权限未变化,静默跳过 continue; } - - execSync(`chmod 755 ${filePath}`); - log_success(`chmod finished: "${filePath}"`); - - // 更新哈希缓存 + try { + execSync(`chmod 755 ${filePath}`); + log_success(`chmod finished: "${filePath}"`); + } catch (e) { + log_error(`chmod failed for ${filePath}:`, e.message); + } hashCache[cacheKey] = currentHash; hasChanges = true; } @@ -578,34 +573,22 @@ const resolveServicePermission = async () => { } }; -// 在 resolveResource 函数后添加新函数 +// resolve locales (从 src/locales 复制到 resources/locales,并使用 hash 检查) async function resolveLocales() { const srcLocalesDir = path.join(cwd, "src/locales"); const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales"); try { - // 确保目标目录存在 await fsp.mkdir(targetLocalesDir, { recursive: true }); - - // 读取所有语言文件 const files = await fsp.readdir(srcLocalesDir); - - // 复制每个文件,只有当哈希变化时才复制 for (const file of files) { const srcPath = path.join(srcLocalesDir, file); const targetPath = path.join(targetLocalesDir, file); - - // 检查文件是否需要更新 - if (!(await hasFileChanged(srcPath, targetPath))) { - // 文件未变化,静默跳过 - continue; - } - + if (!(await hasFileChanged(srcPath, targetPath))) continue; await fsp.copyFile(srcPath, targetPath); await updateHashCache(targetPath); log_success(`Copied locale file: ${file}`); } - log_success("All locale files processed successfully"); } catch (err) { log_error("Error copying locale files:", err.message); @@ -613,34 +596,30 @@ async function resolveLocales() { } } -/** - * main - */ +// ======================= +// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy) +// ======================= const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`; - const resolveService = () => { let ext = platform === "win32" ? ".exe" : ""; let suffix = platform === "linux" ? "-" + SIDECAR_HOST : ""; - resolveResource({ + return resolveResource({ file: "clash-verge-service" + suffix + ext, downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`, }); }; - const resolveInstall = () => { let ext = platform === "win32" ? ".exe" : ""; let suffix = platform === "linux" ? "-" + SIDECAR_HOST : ""; - resolveResource({ + return resolveResource({ file: "clash-verge-service-install" + suffix + ext, downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`, }); }; - const resolveUninstall = () => { let ext = platform === "win32" ? ".exe" : ""; let suffix = platform === "linux" ? "-" + SIDECAR_HOST : ""; - - resolveResource({ + return resolveResource({ file: "clash-verge-service-uninstall" + suffix + ext, downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`, }); @@ -666,15 +645,27 @@ const resolveEnableLoopback = () => file: "enableLoopback.exe", downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`, }); - const resolveWinSysproxy = () => resolveResource({ file: "sysproxy.exe", downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`, }); +const resolveSetDnsScript = () => + resolveResource({ + file: "set_dns.sh", + localPath: path.join(cwd, "scripts/set_dns.sh"), + }); +const resolveUnSetDnsScript = () => + resolveResource({ + file: "unset_dns.sh", + localPath: path.join(cwd, "scripts/unset_dns.sh"), + }); + +// ======================= +// Tasks +// ======================= const tasks = [ - // { name: "clash", func: resolveClash, retry: 5 }, { name: "verge-mihomo-alpha", func: () => @@ -724,11 +715,7 @@ const tasks = [ retry: 5, macosOnly: true, }, - { - name: "locales", - func: resolveLocales, - retry: 2, - }, + { name: "locales", func: resolveLocales, retry: 2 }, ]; async function runTask() { diff --git a/scripts/telegram.mjs b/scripts/telegram.mjs index d7c741fc..f1240494 100644 --- a/scripts/telegram.mjs +++ b/scripts/telegram.mjs @@ -1,6 +1,6 @@ import axios from "axios"; import { readFileSync } from "fs"; -import { log_success, log_error, log_info } from "./utils.mjs"; +import { log_error, log_info, log_success } from "./utils.mjs"; const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道 const CHAT_ID_TEST = "@vergetest"; // 测试频道 @@ -71,6 +71,19 @@ async function sendTelegramNotification() { .join("\n"); } + function normalizeDetailsTags(content) { + return content + .replace( + /\s*\s*(.*?)\s*<\/strong>\s*<\/summary>/g, + "\n$1\n", + ) + .replace(/\s*(.*?)\s*<\/summary>/g, "\n$1\n") + .replace(/<\/?details>/g, "") + .replace(/<\/?strong>/g, (m) => (m === "" ? "" : "")) + .replace(//g, "\n"); + } + + releaseContent = normalizeDetailsTags(releaseContent); const formattedContent = convertMarkdownToTelegramHTML(releaseContent); const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布"; diff --git a/src-tauri/.clippy.toml b/src-tauri/.clippy.toml index 0e7ffd07..a1db40ca 100644 --- a/src-tauri/.clippy.toml +++ b/src-tauri/.clippy.toml @@ -1 +1,2 @@ -avoid-breaking-exported-api = true \ No newline at end of file +avoid-breaking-exported-api = true +cognitive-complexity-threshold = 25 \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9b55a1a6..4c01e031 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -152,6 +152,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" @@ -473,7 +479,7 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.32", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -488,25 +494,23 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.9" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "async-trait", - "axum-core 0.4.5", + "axum-core 0.5.5", "bytes", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper 1.0.2", "tower 0.5.2", "tower-layer", @@ -532,19 +536,17 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper 1.0.2", "tower-layer", "tower-service", @@ -1062,18 +1064,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstyle", "clap_lex", @@ -1101,17 +1103,13 @@ dependencies = [ "compact_str", "console-subscriber", "criterion", - "dashmap 6.1.0", "deelevate", "delay_timer", - "dirs 6.0.0", "dunce", "flexi_logger", "futures", "gethostname", "getrandom 0.3.4", - "hex", - "hmac", "isahc", "libc", "log", @@ -1130,7 +1128,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "sha2 0.10.9", "signal-hook 0.3.18", "smartstring", "sys-locale", @@ -1164,18 +1161,21 @@ dependencies = [ [[package]] name = "clash_verge_logger" -version = "0.1.0" -source = "git+https://github.com/clash-verge-rev/clash-verge-logger#256dc7441f3d0a0c1faa89e345379b32308bc815" +version = "0.2.0" +source = "git+https://github.com/clash-verge-rev/clash-verge-logger#9bb189b5b5c4c2eee35168ff4997e8fb10901c81" dependencies = [ + "arraydeque", + "compact_str", "flexi_logger", "log", "nu-ansi-term", + "tokio", ] [[package]] name = "clash_verge_service_ipc" -version = "2.0.17" -source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#4cd8614fbe341fdf6a41b931ec4d500cbaa8dfde" +version = "2.0.21" +source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#1e34c648e48f8580208ff777686092e0a94b8025" dependencies = [ "anyhow", "compact_str", @@ -1256,9 +1256,11 @@ dependencies = [ "castaway 0.2.4", "cfg-if", "itoa", + "rkyv", "rustversion", "ryu", "serde", + "smallvec", "static_assertions", ] @@ -1283,22 +1285,23 @@ dependencies = [ [[package]] name = "console-api" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +checksum = "e8599749b6667e2f0c910c1d0dff6901163ff698a52d5a39720f61b5be4b20d3" dependencies = [ "futures-core", - "prost 0.13.5", - "prost-types 0.13.5", - "tonic 0.12.3", + "prost 0.14.1", + "prost-types 0.14.1", + "tonic 0.14.2", + "tonic-prost", "tracing-core", ] [[package]] name = "console-subscriber" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +checksum = "fb4915b7d8dd960457a1b6c380114c2944f728e7c65294ab247ae6b6f1f37592" dependencies = [ "console-api", "crossbeam-channel", @@ -1307,14 +1310,14 @@ dependencies = [ "hdrhistogram", "humantime", "hyper-util", - "prost 0.13.5", - "prost-types 0.13.5", + "prost 0.14.1", + "prost-types 0.14.1", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic 0.12.3", + "tonic 0.14.2", "tracing", "tracing-core", "tracing-subscriber", @@ -3402,12 +3405,13 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", @@ -3415,9 +3419,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" dependencies = [ "displaydoc", "icu_collections", @@ -3438,9 +3442,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" dependencies = [ "displaydoc", "icu_collections", @@ -3460,14 +3464,14 @@ checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", + "serde", "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -3781,9 +3785,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -3976,9 +3980,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" @@ -4124,6 +4128,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -4215,9 +4225,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", @@ -4244,6 +4254,26 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -5472,9 +5502,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -5586,12 +5616,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", - "prost-derive 0.13.5", + "prost-derive 0.14.1", ] [[package]] @@ -5609,9 +5639,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools 0.14.0", @@ -5631,11 +5661,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ - "prost 0.13.5", + "prost 0.14.1", ] [[package]] @@ -5644,6 +5674,26 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "publicsuffix" version = "2.3.0" @@ -5757,6 +5807,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + [[package]] name = "rand" version = "0.7.3" @@ -5990,14 +6049,20 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "regress" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145bb27393fe455dd64d6cbc8d059adfa392590a45eadf079c01b11857e7b010" +checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.0", "memchr", ] +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" + [[package]] name = "reqwest" version = "0.12.24" @@ -6117,6 +6182,30 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "rkyv" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" +dependencies = [ + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "rs-snowflake" version = "0.6.0" @@ -6225,9 +6314,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -6235,9 +6324,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -7047,16 +7136,16 @@ dependencies = [ [[package]] name = "sysproxy" -version = "0.3.0" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#9fe61ca25dc5808cb6d7f13ae73a7a250ab56173" +version = "0.3.1" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#50100ab03eb802056c381f3c5009e903c67e3bac" dependencies = [ "interfaces", "iptools", "log", - "thiserror 1.0.69", + "thiserror 2.0.17", "url", - "windows 0.58.0", - "winreg 0.52.0", + "windows 0.62.2", + "winreg 0.55.0", "xdg", ] @@ -7176,9 +7265,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9871670c6711f50fddd4e20350be6b9dd6e6c2b5d77d8ee8900eb0d58cd837a" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" dependencies = [ "anyhow", "bytes", @@ -7310,9 +7399,9 @@ dependencies = [ [[package]] name = "tauri-plugin-autostart" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062cdcd483d5e3148c9a64dabf8c574e239e2aa1193cf208d95cf89a676f87a5" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" dependencies = [ "auto-launch", "serde", @@ -7324,9 +7413,9 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97386ff464c30f491847e56355e9f3bd7ce82726c8c51c4ca93dc6bdb7993751" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" dependencies = [ "arboard", "log", @@ -7339,9 +7428,9 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" -version = "2.4.4" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd913b8b96627ec87a847ee4fe101427d95100f6c565768c2361c47b70d02bff" +checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73" dependencies = [ "dunce", "plist", @@ -7387,9 +7476,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34cadb63450580599193ebe3d69ce292888f1b56c26ea63563ff302f8fdf1f7" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" dependencies = [ "log", "raw-window-handle", @@ -7405,9 +7494,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.4.3" +version = "2.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2864ee9907a87907ad710b5eab081a34c3c812af961d154976dab87f1fe39d12" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" dependencies = [ "anyhow", "dunce", @@ -7427,9 +7516,9 @@ dependencies = [ [[package]] name = "tauri-plugin-global-shortcut" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df9f0f7bf2fe768b85fee4951c2505a35b72c44df1f6403e74e110bc13c5f58" +checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" dependencies = [ "global-hotkey", "log", @@ -7442,9 +7531,9 @@ dependencies = [ [[package]] name = "tauri-plugin-http" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c51b7e91dd890ba1951c17ad35fd78eebb4da4fdd55347898faef546794e20f" +checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70" dependencies = [ "bytes", "cookie_store", @@ -7491,9 +7580,9 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec42df990633cbe5148ae502b06421ab2851b890f1655965c7413fb4eb11f73" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" dependencies = [ "log", "notify-rust", @@ -7510,9 +7599,9 @@ dependencies = [ [[package]] name = "tauri-plugin-process" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" dependencies = [ "tauri", "tauri-plugin", @@ -7520,9 +7609,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63110fea291eb6d9eb953fcb0455178257774161317ce3b87c082e81ef6776c6" +checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" dependencies = [ "encoding_rs", "log", @@ -7573,9 +7662,9 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ "bitflags 2.10.0", "log", @@ -7900,11 +7989,12 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -8153,13 +8243,12 @@ dependencies = [ [[package]] name = "tonic" -version = "0.12.3" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ - "async-stream", "async-trait", - "axum 0.7.9", + "axum 0.8.6", "base64 0.22.1", "bytes", "h2 0.4.12", @@ -8171,11 +8260,11 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost 0.13.5", - "socket2 0.5.10", + "socket2 0.6.1", + "sync_wrapper 1.0.2", "tokio", "tokio-stream", - "tower 0.4.13", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -8194,6 +8283,17 @@ dependencies = [ "tonic 0.10.2", ] +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost 0.14.1", + "tonic 0.14.2", +] + [[package]] name = "tonic-web" version = "0.10.2" @@ -8242,11 +8342,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.0", "pin-project-lite", + "slab", "sync_wrapper 1.0.2", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -8526,9 +8630,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -8764,9 +8868,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -8775,25 +8879,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.108", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -8804,9 +8894,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8814,22 +8904,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.108", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -8922,9 +9012,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -9003,8 +9093,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", ] [[package]] @@ -9099,27 +9189,29 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -9132,16 +9224,12 @@ dependencies = [ ] [[package]] -name = "windows-core" -version = "0.58.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -9150,8 +9238,8 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -9163,8 +9251,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -9178,18 +9266,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -9203,17 +9291,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -9247,6 +9324,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -9258,15 +9345,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -9285,16 +9363,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -9439,6 +9507,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -9655,16 +9732,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -9708,9 +9775,9 @@ checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" @@ -9808,9 +9875,9 @@ dependencies = [ [[package]] name = "xdg" -version = "2.5.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" [[package]] name = "xkeysym" @@ -9820,9 +9887,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xsum" @@ -9832,11 +9899,10 @@ checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -9844,9 +9910,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -9979,9 +10045,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -9990,10 +10056,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -10001,9 +10068,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -10057,9 +10124,9 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f5c0e0be..b5e3a424 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,6 @@ tauri-build = { version = "2.5.1", features = [] } [dependencies] warp = { version = "0.4.2", features = ["server"] } anyhow = "1.0.100" -dirs = "6.0" open = "5.3.2" log = "0.4.28" dunce = "1.0.5" @@ -43,7 +42,7 @@ serde = { version = "1.0.228", features = ["derive"] } reqwest = { version = "0.12.24", features = ["json", "cookies"] } regex = "1.12.2" sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } -tauri = { version = "2.9.1", features = [ +tauri = { version = "2.9.2", features = [ "protocol-asset", "devtools", "tray-icon", @@ -51,13 +50,13 @@ tauri = { version = "2.9.1", features = [ "image-png", ] } network-interface = { version = "2.0.3", features = ["serde"] } -tauri-plugin-shell = "2.3.2" -tauri-plugin-dialog = "2.4.1" -tauri-plugin-fs = "2.4.3" -tauri-plugin-process = "2.3.0" -tauri-plugin-clipboard-manager = "2.3.1" -tauri-plugin-deep-link = "2.4.4" -tauri-plugin-window-state = "2.4.0" +tauri-plugin-shell = "2.3.3" +tauri-plugin-dialog = "2.4.2" +tauri-plugin-fs = "2.4.4" +tauri-plugin-process = "2.3.1" +tauri-plugin-clipboard-manager = "2.3.2" +tauri-plugin-deep-link = "2.4.5" +tauri-plugin-window-state = "2.4.1" zip = "6.0.0" reqwest_dav = "0.2.2" aes-gcm = { version = "0.10.3", features = ["std"] } @@ -67,12 +66,8 @@ futures = "0.3.31" sys-locale = "0.3.2" libc = "0.2.177" gethostname = "1.1.0" -hmac = "0.12.1" -sha2 = "0.10.9" -hex = "0.4.3" scopeguard = "1.2.0" -dashmap = "6.1.0" -tauri-plugin-notification = "2.3.2" +tauri-plugin-notification = "2.3.3" tokio-stream = "0.1.17" isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding", @@ -80,20 +75,17 @@ isahc = { version = "1.7.2", default-features = false, features = [ ] } backoff = { version = "0.4.0", features = ["tokio"] } compact_str = { version = "0.9.0", features = ["serde"] } -tauri-plugin-http = "2.5.3" +tauri-plugin-http = "2.5.4" flexi_logger = "0.31.7" -console-subscriber = { version = "0.4.1", optional = true } +console-subscriber = { version = "0.5.0", optional = true } tauri-plugin-devtools = { version = "2.0.1" } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" } clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" } async-trait = "0.1.89" smartstring = { version = "1.0.1", features = ["serde"] } -clash_verge_service_ipc = { version = "2.0.17", features = [ +clash_verge_service_ipc = { version = "2.0.21", features = [ "client", ], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" } -# clash_verge_service_ipc = { version = "2.0.17", features = [ -# "client", -# ], path = "../../clash-verge-service-ipc" } [target.'cfg(windows)'.dependencies] runas = "=1.2.0" @@ -126,8 +118,8 @@ users = "0.11.0" signal-hook = "0.3.18" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -tauri-plugin-autostart = "2.5.0" -tauri-plugin-global-shortcut = "2.3.0" +tauri-plugin-autostart = "2.5.1" +tauri-plugin-global-shortcut = "2.3.1" tauri-plugin-updater = "2.9.0" [features] @@ -232,3 +224,4 @@ needless_raw_string_hashes = "deny" # Too many in existing code #restriction = { level = "allow", priority = -1 } or_fun_call = "deny" +cognitive_complexity = "deny" diff --git a/src-tauri/benches/draft_benchmark.rs b/src-tauri/benches/draft_benchmark.rs index 694fc82a..3942382f 100644 --- a/src-tauri/benches/draft_benchmark.rs +++ b/src-tauri/benches/draft_benchmark.rs @@ -3,7 +3,6 @@ use std::hint::black_box; use std::process; use tokio::runtime::Runtime; -// 引入业务模型 & Draft 实现 use app_lib::config::IVerge; use app_lib::utils::Draft as DraftNew; @@ -17,108 +16,86 @@ fn make_draft() -> DraftNew> { DraftNew::from(verge) } -/// 基准:只读 data_ref(正式数据) -fn bench_data_ref(c: &mut Criterion) { - c.bench_function("draft_data_ref", |b| { - b.iter(|| { - let draft = make_draft(); - let data = draft.data_ref(); - black_box(data.enable_auto_launch); - }); - }); -} - -/// 基准:可写 data_mut(正式数据) -fn bench_data_mut(c: &mut Criterion) { - c.bench_function("draft_data_mut", |b| { - b.iter(|| { - let draft = make_draft(); - let mut data = draft.data_mut(); - data.enable_tun_mode = Some(true); - black_box(data.enable_tun_mode); - }); - }); -} - -/// 基准:首次创建草稿(会触发 clone) -fn bench_draft_mut_first(c: &mut Criterion) { - c.bench_function("draft_draft_mut_first", |b| { - b.iter(|| { - let draft = make_draft(); - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); - black_box(d.enable_auto_launch); - }); - }); -} - -/// 基准:重复 draft_mut(已存在草稿,不再 clone) -fn bench_draft_mut_existing(c: &mut Criterion) { - c.bench_function("draft_draft_mut_existing", |b| { - b.iter(|| { - let draft = make_draft(); - { - let mut first = draft.draft_mut(); - first.enable_tun_mode = Some(true); - } - let mut second = draft.draft_mut(); - second.enable_tun_mode = Some(false); - black_box(second.enable_tun_mode); - }); - }); -} - -/// 基准:零拷贝读取最新视图(latest_ref) -fn bench_latest_ref(c: &mut Criterion) { - c.bench_function("draft_latest_ref", |b| { - b.iter(|| { - let draft = make_draft(); - let latest = draft.latest_ref(); - black_box(latest.enable_auto_launch); - }); - }); -} - -/// 基准:apply(提交草稿) -fn bench_apply(c: &mut Criterion) { - c.bench_function("draft_apply", |b| { - b.iter(|| { - let draft = make_draft(); - { - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); - } - let _ = draft.apply(); - }); - }); -} - -/// 基准:discard(丢弃草稿) -fn bench_discard(c: &mut Criterion) { - c.bench_function("draft_discard", |b| { - b.iter(|| { - let draft = make_draft(); - { - let mut d = draft.draft_mut(); - d.enable_auto_launch = Some(false); - } - let _ = draft.discard(); - }); - }); -} - -/// 基准:异步 with_data_modify -fn bench_with_data_modify(c: &mut Criterion) { - let rt = Runtime::new().unwrap_or_else(|error| { - eprintln!("draft benchmarks require a Tokio runtime: {error}"); +pub fn bench_draft(c: &mut Criterion) { + let rt = Runtime::new().unwrap_or_else(|e| { + eprintln!("Tokio runtime init failed: {e}"); process::exit(1); }); - c.bench_function("draft_with_data_modify", |b| { + let mut group = c.benchmark_group("draft"); + group.sample_size(100); + group.warm_up_time(std::time::Duration::from_millis(300)); + group.measurement_time(std::time::Duration::from_secs(1)); + + group.bench_function("data_mut", |b| { + b.iter(|| { + let draft = black_box(make_draft()); + let mut data = draft.data_mut(); + data.enable_tun_mode = Some(true); + black_box(&data.enable_tun_mode); + }); + }); + + group.bench_function("draft_mut_first", |b| { + b.iter(|| { + let draft = black_box(make_draft()); + let mut d = draft.draft_mut(); + d.enable_auto_launch = Some(false); + black_box(&d.enable_auto_launch); + }); + }); + + group.bench_function("draft_mut_existing", |b| { + b.iter(|| { + let draft = black_box(make_draft()); + { + let mut first = draft.draft_mut(); + first.enable_tun_mode = Some(true); + black_box(&first.enable_tun_mode); + } + let mut second = draft.draft_mut(); + second.enable_tun_mode = Some(false); + black_box(&second.enable_tun_mode); + }); + }); + + group.bench_function("latest_ref", |b| { + b.iter(|| { + let draft = black_box(make_draft()); + let latest = draft.latest_ref(); + black_box(&latest.enable_auto_launch); + }); + }); + + group.bench_function("apply", |b| { + b.iter(|| { + let draft = black_box(make_draft()); + { + let mut d = draft.draft_mut(); + d.enable_auto_launch = Some(false); + } + draft.apply(); + black_box(&draft); + }); + }); + + group.bench_function("discard", |b| { + b.iter(|| { + let draft = black_box(make_draft()); + { + let mut d = draft.draft_mut(); + d.enable_auto_launch = Some(false); + } + draft.discard(); + black_box(&draft); + }); + }); + + group.bench_function("with_data_modify_async", |b| { b.to_async(&rt).iter(|| async { - let draft = make_draft(); - let _res: Result<(), anyhow::Error> = draft - .with_data_modify(|mut box_data| async move { + let draft = black_box(make_draft()); + let _: Result<(), anyhow::Error> = draft + .with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move { box_data.enable_auto_launch = Some(!box_data.enable_auto_launch.unwrap_or(false)); Ok((box_data, ())) @@ -126,17 +103,9 @@ fn bench_with_data_modify(c: &mut Criterion) { .await; }); }); + + group.finish(); } -criterion_group!( - benches, - bench_data_ref, - bench_data_mut, - bench_draft_mut_first, - bench_draft_mut_existing, - bench_latest_ref, - bench_apply, - bench_discard, - bench_with_data_modify -); +criterion_group!(benches, bench_draft); criterion_main!(benches); diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 4daa150f..959277fe 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -12,6 +12,7 @@ use smartstring::alias::String; use std::path::Path; use tauri::{AppHandle, Manager}; use tokio::fs; +use tokio::io::AsyncWriteExt; /// 打开应用程序所在目录 #[tauri::command] @@ -41,6 +42,20 @@ pub fn open_web_url(url: String) -> CmdResult<()> { open::that(url.as_str()).stringify_err() } +// TODO 后续可以为前端提供接口,当前作为托盘菜单使用 +/// 打开 Verge 最新日志 +#[tauri::command] +pub async fn open_app_log() -> CmdResult<()> { + open::that(dirs::app_latest_log().stringify_err()?).stringify_err() +} + +// TODO 后续可以为前端提供接口,当前作为托盘菜单使用 +/// 打开 Clash 最新日志 +#[tauri::command] +pub async fn open_core_log() -> CmdResult<()> { + open::that(dirs::clash_latest_log().stringify_err()?).stringify_err() +} + /// 打开/关闭开发者工具 #[tauri::command] pub fn open_devtools(app_handle: AppHandle) { @@ -102,7 +117,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult } if !icon_cache_dir.exists() { - let _ = std::fs::create_dir_all(&icon_cache_dir); + let _ = fs::create_dir_all(&icon_cache_dir).await; } let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str())); @@ -126,7 +141,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult if is_image && !is_html { { - let mut file = match std::fs::File::create(&temp_path) { + let mut file = match fs::File::create(&temp_path).await { Ok(file) => file, Err(_) => { if icon_path.exists() { @@ -135,12 +150,12 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult return Err("Failed to create temporary file".into()); } }; - - std::io::copy(&mut content.as_ref(), &mut file).stringify_err()?; + file.write_all(content.as_ref()).await.stringify_err()?; + file.flush().await.stringify_err()?; } if !icon_path.exists() { - match std::fs::rename(&temp_path, &icon_path) { + match fs::rename(&temp_path, &icon_path).await { Ok(_) => {} Err(_) => { let _ = temp_path.remove_if_exists().await; @@ -226,7 +241,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult CmdResult<()> { - log::info!(target: "app", "前端UI已准备就绪"); + logging!(info, Type::Cmd, "前端UI已准备就绪"); crate::utils::resolve::ui::mark_ui_ready(); Ok(()) } @@ -234,7 +249,7 @@ pub fn notify_ui_ready() -> CmdResult<()> { /// UI加载阶段 #[tauri::command] pub fn update_ui_stage(stage: String) -> CmdResult<()> { - log::info!(target: "app", "UI加载阶段更新: {}", stage.as_str()); + logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str()); use crate::utils::resolve::ui::UiReadyStage; @@ -245,7 +260,12 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> { "ResourcesLoaded" => UiReadyStage::ResourcesLoaded, "Ready" => UiReadyStage::Ready, _ => { - log::warn!(target: "app", "未知的UI加载阶段: {}", stage.as_str()); + logging!( + warn, + Type::Cmd, + "Warning: 未知的UI加载阶段: {}", + stage.as_str() + ); return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into()); } }; diff --git a/src-tauri/src/cmd/backup.rs b/src-tauri/src/cmd/backup.rs index 8610b682..a105f4d8 100644 --- a/src-tauri/src/cmd/backup.rs +++ b/src-tauri/src/cmd/backup.rs @@ -11,8 +11,8 @@ pub async fn create_local_backup() -> CmdResult<()> { /// List local backups #[tauri::command] -pub fn list_local_backup() -> CmdResult> { - feat::list_local_backup().stringify_err() +pub async fn list_local_backup() -> CmdResult> { + feat::list_local_backup().await.stringify_err() } /// Delete local backup @@ -29,6 +29,8 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> { /// Export local backup to a user selected destination #[tauri::command] -pub fn export_local_backup(filename: String, destination: String) -> CmdResult<()> { - feat::export_local_backup(filename, destination).stringify_err() +pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> { + feat::export_local_backup(filename, destination) + .await + .stringify_err() } diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 09f5e101..1bcadea9 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,15 +1,16 @@ -use std::collections::VecDeque; - use super::CmdResult; +use crate::utils::dirs; use crate::{ cmd::StringifyErr, config::Config, + constants, core::{CoreManager, handle, validate::CoreConfigValidator}, }; use crate::{config::*, feat, logging, utils::logging::Type}; use compact_str::CompactString; use serde_yaml_ng::Mapping; use smartstring::alias::String; +use tokio::fs; /// 复制Clash环境变量 #[tauri::command] @@ -42,10 +43,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult { pub async fn change_clash_core(clash_core: String) -> CmdResult> { logging!(info, Type::Config, "changing core to {clash_core}"); - match CoreManager::global() - .change_core(Some(clash_core.clone())) - .await - { + match CoreManager::global().change_core(&clash_core).await { Ok(_) => { // 切换内核后重启内核 match CoreManager::global().restart_core().await { @@ -113,7 +111,7 @@ pub async fn test_delay(url: String) -> CmdResult { let result = match feat::test_delay(url).await { Ok(delay) => delay, Err(e) => { - log::error!(target: "app", "{}", e); + logging!(error, Type::Cmd, "{}", e); 10000u32 } }; @@ -130,7 +128,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { // 获取DNS配置文件路径 let dns_path = dirs::app_home_dir() .stringify_err()? - .join("dns_config.yaml"); + .join(constants::files::DNS_CONFIG); // 保存DNS配置到文件 let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?; @@ -153,18 +151,16 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { // 读取DNS配置文件 let dns_path = dirs::app_home_dir() .stringify_err()? - .join("dns_config.yaml"); + .join(constants::files::DNS_CONFIG); if !dns_path.exists() { logging!(warn, Type::Config, "DNS config file not found"); return Err("DNS config file not found".into()); } - let dns_yaml = tokio::fs::read_to_string(&dns_path) - .await - .stringify_err_log(|e| { - logging!(error, Type::Config, "Failed to read DNS config: {e}"); - })?; + let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| { + logging!(error, Type::Config, "Failed to read DNS config: {e}"); + })?; // 解析DNS配置 let patch_config = serde_yaml_ng::from_str::(&dns_yaml) @@ -233,7 +229,7 @@ pub fn check_dns_config_exists() -> CmdResult { let dns_path = dirs::app_home_dir() .stringify_err()? - .join("dns_config.yaml"); + .join(constants::files::DNS_CONFIG); Ok(dns_path.exists()) } @@ -246,7 +242,7 @@ pub async fn get_dns_config_content() -> CmdResult { let dns_path = dirs::app_home_dir() .stringify_err()? - .join("dns_config.yaml"); + .join(constants::files::DNS_CONFIG); if !fs::try_exists(&dns_path).await.stringify_err()? { return Err("DNS config file not found".into()); @@ -259,10 +255,8 @@ pub async fn get_dns_config_content() -> CmdResult { /// 验证DNS配置文件 #[tauri::command] pub async fn validate_dns_config() -> CmdResult<(bool, String)> { - use crate::utils::dirs; - let app_dir = dirs::app_home_dir().stringify_err()?; - let dns_path = app_dir.join("dns_config.yaml"); + let dns_path = app_dir.join(constants::files::DNS_CONFIG); let dns_path_str = dns_path.to_str().unwrap_or_default(); if !dns_path.exists() { @@ -275,7 +269,7 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> { } #[tauri::command] -pub async fn get_clash_logs() -> CmdResult> { +pub async fn get_clash_logs() -> CmdResult> { let logs = CoreManager::global() .get_clash_logs() .await diff --git a/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs b/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs index d13133f2..dc18978e 100644 --- a/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs +++ b/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs @@ -6,6 +6,7 @@ use crate::{logging, utils::logging::Type}; use super::UnlockItem; use super::utils::{country_code_to_emoji, get_local_date_string}; +#[allow(clippy::cognitive_complexity)] pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem { let device_api_url = "https://disney.api.edge.bamgrid.com/devices"; let auth_header = diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index 120bd1f0..c0dedfde 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -2,13 +2,14 @@ use super::CmdResult; use crate::cmd::StringifyErr; use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; use crate::process::AsyncHandler; +use crate::{logging, utils::logging::Type}; use network_interface::NetworkInterface; use serde_yaml_ng::Mapping; /// get the system proxy #[tauri::command] pub async fn get_sys_proxy() -> CmdResult { - log::debug!(target: "app", "异步获取系统代理配置"); + logging!(debug, Type::Network, "异步获取系统代理配置"); let current = AsyncProxyQuery::get_system_proxy().await; @@ -20,14 +21,21 @@ pub async fn get_sys_proxy() -> CmdResult { ); map.insert("bypass".into(), current.bypass.into()); - log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port); + logging!( + debug, + Type::Network, + "返回系统代理配置: enable={}, {}:{}", + current.enable, + current.host, + current.port + ); Ok(map) } /// 获取自动代理配置 #[tauri::command] pub async fn get_auto_proxy() -> CmdResult { - log::debug!(target: "app", "开始获取自动代理配置(事件驱动)"); + logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)"); let proxy_manager = EventDrivenProxyManager::global(); @@ -41,7 +49,13 @@ pub async fn get_auto_proxy() -> CmdResult { map.insert("enable".into(), current.enable.into()); map.insert("url".into(), current.url.clone().into()); - log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url); + logging!( + debug, + Type::Network, + "返回自动代理配置(缓存): enable={}, url={}", + current.enable, + current.url + ); Ok(map) } diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 766cea73..949ddce1 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -85,7 +85,7 @@ pub async fn enhance_profiles() -> CmdResult { match feat::enhance_profiles().await { Ok(_) => {} Err(e) => { - log::error!(target: "app", "{}", e); + logging!(error, Type::Cmd, "{}", e); return Err(e.to_string().into()); } } @@ -99,7 +99,7 @@ pub async fn import_profile(url: std::string::String, option: Option) logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url); // 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹 - let item = match PrfItem::from_url(&url, None, None, option).await { + let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await { Ok(it) => { logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置"); it @@ -110,7 +110,7 @@ pub async fn import_profile(url: std::string::String, option: Option) } }; - match profiles_append_item_safe(item.clone()).await { + match profiles_append_item_safe(item).await { Ok(_) => match profiles_save_file_safe().await { Ok(_) => { logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功"); @@ -145,13 +145,13 @@ pub async fn import_profile(url: std::string::String, option: Option) /// 调整profile的顺序 #[tauri::command] pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { - match profiles_reorder_safe(active_id, over_id).await { + match profiles_reorder_safe(&active_id, &over_id).await { Ok(_) => { - log::info!(target: "app", "重新排序配置文件"); + logging!(info, Type::Cmd, "重新排序配置文件"); Ok(()) } Err(err) => { - log::error!(target: "app", "重新排序配置文件失败: {}", err); + logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err); Err(format!("重新排序配置文件失败: {}", err).into()) } } @@ -161,7 +161,7 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { /// 创建一个新的配置文件 #[tauri::command] pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { - match profiles_append_item_with_filedata_safe(item.clone(), file_data).await { + match profiles_append_item_with_filedata_safe(&item, file_data).await { Ok(_) => { // 发送配置变更通知 if let Some(uid) = &item.uid { @@ -180,10 +180,10 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu /// 更新配置文件 #[tauri::command] pub async fn update_profile(index: String, option: Option) -> CmdResult { - match feat::update_profile(index, option, Some(true)).await { + match feat::update_profile(&index, option.as_ref(), true, true).await { Ok(_) => Ok(()), Err(e) => { - log::error!(target: "app", "{}", e); + logging!(error, Type::Cmd, "{}", e); Err(e.to_string().into()) } } @@ -194,9 +194,7 @@ pub async fn update_profile(index: String, option: Option) -> CmdResu pub async fn delete_profile(index: String) -> CmdResult { println!("delete_profile: {}", index); // 使用Send-safe helper函数 - let should_update = profiles_delete_item_safe(index.clone()) - .await - .stringify_err()?; + let should_update = profiles_delete_item_safe(&index).await.stringify_err()?; profiles_save_file_safe().await.stringify_err()?; if should_update { @@ -208,7 +206,7 @@ pub async fn delete_profile(index: String) -> CmdResult { handle::Handle::notify_profile_changed(index); } Err(e) => { - log::error!(target: "app", "{}", e); + logging!(error, Type::Cmd, "{}", e); return Err(e.to_string().into()); } } @@ -216,6 +214,257 @@ pub async fn delete_profile(index: String) -> CmdResult { Ok(()) } +/// 验证新配置文件的语法 +async fn validate_new_profile(new_profile: &String) -> Result<(), ()> { + logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile); + + // 获取目标配置文件路径 + let config_file_result = { + let profiles_config = Config::profiles().await; + let profiles_data = profiles_config.latest_ref(); + match profiles_data.get_item(new_profile) { + Ok(item) => { + if let Some(file) = &item.file { + let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str())); + path.ok() + } else { + None + } + } + Err(e) => { + logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e); + None + } + } + }; + + // 如果获取到文件路径,检查YAML语法 + if let Some(file_path) = config_file_result { + if !file_path.exists() { + logging!( + error, + Type::Cmd, + "目标配置文件不存在: {}", + file_path.display() + ); + handle::Handle::notice_message( + "config_validate::file_not_found", + format!("{}", file_path.display()), + ); + return Err(()); + } + + // 超时保护 + let file_read_result = tokio::time::timeout( + Duration::from_secs(5), + tokio::fs::read_to_string(&file_path), + ) + .await; + + match file_read_result { + Ok(Ok(content)) => { + let yaml_parse_result = AsyncHandler::spawn_blocking(move || { + serde_yaml_ng::from_str::(&content) + }) + .await; + + match yaml_parse_result { + Ok(Ok(_)) => { + logging!(info, Type::Cmd, "目标配置文件语法正确"); + Ok(()) + } + Ok(Err(err)) => { + let error_msg = format!(" {err}"); + logging!( + error, + Type::Cmd, + "目标配置文件存在YAML语法错误:{}", + error_msg + ); + handle::Handle::notice_message( + "config_validate::yaml_syntax_error", + error_msg.clone(), + ); + Err(()) + } + Err(join_err) => { + let error_msg = format!("YAML解析任务失败: {join_err}"); + logging!(error, Type::Cmd, "{}", error_msg); + handle::Handle::notice_message( + "config_validate::yaml_parse_error", + error_msg.clone(), + ); + Err(()) + } + } + } + Ok(Err(err)) => { + let error_msg = format!("无法读取目标配置文件: {err}"); + logging!(error, Type::Cmd, "{}", error_msg); + handle::Handle::notice_message( + "config_validate::file_read_error", + error_msg.clone(), + ); + Err(()) + } + Err(_) => { + let error_msg = "读取配置文件超时(5秒)".to_string(); + logging!(error, Type::Cmd, "{}", error_msg); + handle::Handle::notice_message( + "config_validate::file_read_timeout", + error_msg.clone(), + ); + Err(()) + } + } + } else { + Ok(()) + } +} + +/// 执行配置更新并处理结果 +async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> { + logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile); + let restore_profiles = IProfiles { + current: Some(prev_profile), + items: None, + }; + Config::profiles() + .await + .draft_mut() + .patch_config(restore_profiles) + .stringify_err()?; + Config::profiles().await.apply(); + crate::process::AsyncHandler::spawn(|| async move { + if let Err(e) = profiles_save_file_safe().await { + logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}"); + } + }); + logging!(info, Type::Cmd, "成功恢复到之前的配置"); + Ok(()) +} + +async fn handle_success(current_sequence: u64, current_value: Option) -> CmdResult { + let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); + if current_sequence < latest_sequence { + logging!( + info, + Type::Cmd, + "内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果", + current_sequence, + latest_sequence + ); + Config::profiles().await.discard(); + return Ok(false); + } + + logging!( + info, + Type::Cmd, + "配置更新成功,序列号: {}", + current_sequence + ); + Config::profiles().await.apply(); + handle::Handle::refresh_clash(); + + if let Err(e) = Tray::global().update_tooltip().await { + logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}"); + } + + if let Err(e) = Tray::global().update_menu().await { + logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}"); + } + + if let Err(e) = profiles_save_file_safe().await { + logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}"); + } + + if let Some(current) = ¤t_value { + logging!( + info, + Type::Cmd, + "向前端发送配置变更事件: {}, 序列号: {}", + current, + current_sequence + ); + handle::Handle::notify_profile_changed(current.clone()); + } + + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); + Ok(true) +} + +async fn handle_validation_failure( + error_msg: String, + current_profile: Option, +) -> CmdResult { + logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg); + Config::profiles().await.discard(); + if let Some(prev_profile) = current_profile { + restore_previous_profile(prev_profile).await?; + } + handle::Handle::notice_message("config_validate::error", error_msg); + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); + Ok(false) +} + +async fn handle_update_error(e: E, current_sequence: u64) -> CmdResult { + logging!( + warn, + Type::Cmd, + "更新过程发生错误: {}, 序列号: {}", + e, + current_sequence + ); + Config::profiles().await.discard(); + handle::Handle::notice_message("config_validate::boot_error", e.to_string()); + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); + Ok(false) +} + +async fn handle_timeout(current_profile: Option, current_sequence: u64) -> CmdResult { + let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞"; + logging!( + error, + Type::Cmd, + "{}, 序列号: {}", + timeout_msg, + current_sequence + ); + Config::profiles().await.discard(); + if let Some(prev_profile) = current_profile { + restore_previous_profile(prev_profile).await?; + } + handle::Handle::notice_message("config_validate::timeout", timeout_msg); + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); + Ok(false) +} + +async fn perform_config_update( + current_sequence: u64, + current_value: Option, + current_profile: Option, +) -> CmdResult { + logging!( + info, + Type::Cmd, + "开始内核配置更新,序列号: {}", + current_sequence + ); + let update_result = tokio::time::timeout( + Duration::from_secs(30), + CoreManager::global().update_config(), + ) + .await; + + match update_result { + Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await, + Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await, + Ok(Err(e)) => handle_update_error(e, current_sequence).await, + Err(_) => handle_timeout(current_profile, current_sequence).await, + } +} + /// 修改profiles的配置 #[tauri::command] pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { @@ -256,108 +505,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 如果要切换配置,先检查目标配置文件是否有语法错误 if let Some(new_profile) = profiles.current.as_ref() && current_profile.as_ref() != Some(new_profile) + && validate_new_profile(new_profile).await.is_err() { - logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile); - - // 获取目标配置文件路径 - let config_file_result = { - let profiles_config = Config::profiles().await; - let profiles_data = profiles_config.latest_ref(); - match profiles_data.get_item(new_profile) { - Ok(item) => { - if let Some(file) = &item.file { - let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str())); - path.ok() - } else { - None - } - } - Err(e) => { - logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e); - None - } - } - }; - - // 如果获取到文件路径,检查YAML语法 - if let Some(file_path) = config_file_result { - if !file_path.exists() { - logging!( - error, - Type::Cmd, - "目标配置文件不存在: {}", - file_path.display() - ); - handle::Handle::notice_message( - "config_validate::file_not_found", - format!("{}", file_path.display()), - ); - return Ok(false); - } - - // 超时保护 - let file_read_result = tokio::time::timeout( - Duration::from_secs(5), - tokio::fs::read_to_string(&file_path), - ) - .await; - - match file_read_result { - Ok(Ok(content)) => { - let yaml_parse_result = AsyncHandler::spawn_blocking(move || { - serde_yaml_ng::from_str::(&content) - }) - .await; - - match yaml_parse_result { - Ok(Ok(_)) => { - logging!(info, Type::Cmd, "目标配置文件语法正确"); - } - Ok(Err(err)) => { - let error_msg = format!(" {err}"); - logging!( - error, - Type::Cmd, - "目标配置文件存在YAML语法错误:{}", - error_msg - ); - handle::Handle::notice_message( - "config_validate::yaml_syntax_error", - error_msg.clone(), - ); - return Ok(false); - } - Err(join_err) => { - let error_msg = format!("YAML解析任务失败: {join_err}"); - logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::yaml_parse_error", - error_msg.clone(), - ); - return Ok(false); - } - } - } - Ok(Err(err)) => { - let error_msg = format!("无法读取目标配置文件: {err}"); - logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::file_read_error", - error_msg.clone(), - ); - return Ok(false); - } - Err(_) => { - let error_msg = "读取配置文件超时(5秒)".to_string(); - logging!(error, Type::Cmd, "{}", error_msg); - handle::Handle::notice_message( - "config_validate::file_read_timeout", - error_msg.clone(), - ); - return Ok(false); - } - } - } + CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); + return Ok(false); } // 检查请求有效性 @@ -399,163 +550,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { return Ok(false); } - // 为配置更新添加超时保护 - logging!( - info, - Type::Cmd, - "开始内核配置更新,序列号: {}", - current_sequence - ); - let update_result = tokio::time::timeout( - Duration::from_secs(30), // 30秒超时 - CoreManager::global().update_config(), - ) - .await; - - // 更新配置并进行验证 - match update_result { - Ok(Ok((true, _))) => { - // 内核操作完成后再次检查请求有效性 - let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); - if current_sequence < latest_sequence { - logging!( - info, - Type::Cmd, - "内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果", - current_sequence, - latest_sequence - ); - Config::profiles().await.discard(); - return Ok(false); - } - - logging!( - info, - Type::Cmd, - "配置更新成功,序列号: {}", - current_sequence - ); - Config::profiles().await.apply(); - handle::Handle::refresh_clash(); - - // 强制刷新代理缓存,确保profile切换后立即获取最新节点数据 - // crate::process::AsyncHandler::spawn(|| async move { - // if let Err(e) = super::proxy::force_refresh_proxies().await { - // log::warn!(target: "app", "强制刷新代理缓存失败: {e}"); - // } - // }); - - if let Err(e) = Tray::global().update_tooltip().await { - log::warn!(target: "app", "异步更新托盘提示失败: {e}"); - } - - if let Err(e) = Tray::global().update_menu().await { - log::warn!(target: "app", "异步更新托盘菜单失败: {e}"); - } - - // 保存配置文件 - if let Err(e) = profiles_save_file_safe().await { - log::warn!(target: "app", "异步保存配置文件失败: {e}"); - } - - // 立即通知前端配置变更 - if let Some(current) = ¤t_value { - logging!( - info, - Type::Cmd, - "向前端发送配置变更事件: {}, 序列号: {}", - current, - current_sequence - ); - handle::Handle::notify_profile_changed(current.clone()); - } - - CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); - Ok(true) - } - Ok(Ok((false, error_msg))) => { - logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg); - Config::profiles().await.discard(); - // 如果验证失败,恢复到之前的配置 - if let Some(prev_profile) = current_profile { - logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile); - let restore_profiles = IProfiles { - current: Some(prev_profile), - items: None, - }; - // 静默恢复,不触发验证 - Config::profiles() - .await - .draft_mut() - .patch_config(restore_profiles) - .stringify_err()?; - Config::profiles().await.apply(); - - crate::process::AsyncHandler::spawn(|| async move { - if let Err(e) = profiles_save_file_safe().await { - log::warn!(target: "app", "异步保存恢复配置文件失败: {e}"); - } - }); - - logging!(info, Type::Cmd, "成功恢复到之前的配置"); - } - - // 发送验证错误通知 - handle::Handle::notice_message("config_validate::error", error_msg.to_string()); - CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); - Ok(false) - } - Ok(Err(e)) => { - logging!( - warn, - Type::Cmd, - "更新过程发生错误: {}, 序列号: {}", - e, - current_sequence - ); - Config::profiles().await.discard(); - handle::Handle::notice_message("config_validate::boot_error", e.to_string()); - - CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); - Ok(false) - } - Err(_) => { - // 超时处理 - let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞"; - logging!( - error, - Type::Cmd, - "{}, 序列号: {}", - timeout_msg, - current_sequence - ); - Config::profiles().await.discard(); - - if let Some(prev_profile) = current_profile { - logging!( - info, - Type::Cmd, - "超时后尝试恢复到之前的配置: {}, 序列号: {}", - prev_profile, - current_sequence - ); - let restore_profiles = IProfiles { - current: Some(prev_profile), - items: None, - }; - Config::profiles() - .await - .draft_mut() - .patch_config(restore_profiles) - .stringify_err()?; - Config::profiles().await.apply(); - } - - handle::Handle::notice_message("config_validate::timeout", timeout_msg); - CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst); - Ok(false) - } - } + perform_config_update(current_sequence, current_value, current_profile).await } /// 根据profile name修改profiles @@ -575,33 +570,34 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { // 保存修改前检查是否有更新 update_interval let profiles = Config::profiles().await; - let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) { + let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) + && let Some(new_option) = profile.option.as_ref() + { let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); - let new_interval = profile.option.as_ref().and_then(|o| o.update_interval); + let new_interval = new_option.update_interval; let old_allow_auto_update = old_profile .option .as_ref() .and_then(|o| o.allow_auto_update); - let new_allow_auto_update = profile.option.as_ref().and_then(|o| o.allow_auto_update); + let new_allow_auto_update = new_option.allow_auto_update; (old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update) } else { false }; - profiles_patch_item_safe(index.clone(), profile) + profiles_patch_item_safe(&index, &profile) .await .stringify_err()?; // 如果更新间隔或允许自动更新变更,异步刷新定时器 if should_refresh_timer { - let index_clone = index.clone(); crate::process::AsyncHandler::spawn(move || async move { logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器..."); if let Err(e) = crate::core::Timer::global().refresh().await { logging!(error, Type::Timer, "刷新定时器失败: {}", e); } else { // 刷新成功后发送自定义事件,不触发配置重载 - crate::core::handle::Handle::notify_timer_updated(index_clone); + crate::core::handle::Handle::notify_timer_updated(index); } }); } @@ -634,10 +630,15 @@ pub async fn view_profile(index: String) -> CmdResult { /// 读取配置文件内容 #[tauri::command] pub async fn read_profile_file(index: String) -> CmdResult { - let profiles = Config::profiles().await; - let profiles_ref = profiles.latest_ref(); - let item = profiles_ref.get_item(&index).stringify_err()?; - let data = item.read_file().stringify_err()?; + let item = { + let profiles = Config::profiles().await; + let profiles_ref = profiles.latest_ref(); + PrfItem { + file: profiles_ref.get_item(&index).stringify_err()?.file.clone(), + ..Default::default() + } + }; + let data = item.read_file().await.stringify_err()?; Ok(data) } diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index a1cd3474..1e905e63 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -12,28 +12,37 @@ use tokio::fs; /// 保存profiles的配置 #[tauri::command] pub async fn save_profile_file(index: String, file_data: Option) -> CmdResult { - if file_data.is_none() { - return Ok(()); - } + let file_data = match file_data { + Some(d) => d, + None => return Ok(()), + }; - // 在异步操作前完成所有文件操作 - let (file_path, original_content, is_merge_file) = { + // 在异步操作前获取必要元数据并释放锁 + let (rel_path, is_merge_file) = { let profiles = Config::profiles().await; let profiles_guard = profiles.latest_ref(); let item = profiles_guard.get_item(&index).stringify_err()?; - // 确定是否为merge类型文件 let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge"); - let content = item.read_file().stringify_err()?; let path = item.file.clone().ok_or("file field is null")?; - let profiles_dir = dirs::app_profiles_dir().stringify_err()?; - (profiles_dir.join(path.as_str()), content, is_merge) + (path, is_merge) }; + // 读取原始内容(在释放profiles_guard后进行) + let original_content = PrfItem { + file: Some(rel_path.clone()), + ..Default::default() + } + .read_file() + .await + .stringify_err()?; + + let profiles_dir = dirs::app_profiles_dir().stringify_err()?; + let file_path = profiles_dir.join(rel_path.as_str()); + let file_path_str = file_path.to_string_lossy().to_string(); + // 保存新的配置文件 - let file_data = file_data.ok_or("file_data is None")?; fs::write(&file_path, &file_data).await.stringify_err()?; - let file_path_str = file_path.to_string_lossy().to_string(); logging!( info, Type::Config, @@ -42,102 +51,107 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR is_merge_file ); - // 对于 merge 文件,只进行语法验证,不进行后续内核验证 if is_merge_file { - logging!( - info, - Type::Config, - "[cmd配置save] 检测到merge文件,只进行语法验证" - ); - match CoreConfigValidator::validate_config_file(&file_path_str, Some(true)).await { - Ok((true, _)) => { - logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过"); - // 成功后尝试更新整体配置 - match CoreManager::global().update_config().await { - Ok(_) => { - // 配置更新成功,刷新前端 - handle::Handle::refresh_clash(); - } - Err(e) => { - logging!( - warn, - Type::Config, - "[cmd配置save] 更新整体配置时发生错误: {}", - e - ); - } - } - return Ok(()); - } - Ok((false, error_msg)) => { + return handle_merge_file(&file_path_str, &file_path, &original_content).await; + } + + handle_full_validation(&file_path_str, &file_path, &original_content).await +} + +async fn restore_original( + file_path: &std::path::Path, + original_content: &str, +) -> Result<(), String> { + fs::write(file_path, original_content).await.stringify_err() +} + +fn is_script_error(err: &str, file_path_str: &str) -> bool { + file_path_str.ends_with(".js") + || err.contains("Script syntax error") + || err.contains("Script must contain a main function") + || err.contains("Failed to read script file") +} + +async fn handle_merge_file( + file_path_str: &str, + file_path: &std::path::Path, + original_content: &str, +) -> CmdResult { + logging!( + info, + Type::Config, + "[cmd配置save] 检测到merge文件,只进行语法验证" + ); + + match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await { + Ok((true, _)) => { + logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过"); + if let Err(e) = CoreManager::global().update_config().await { logging!( warn, Type::Config, - "[cmd配置save] merge文件语法验证失败: {}", - error_msg + "[cmd配置save] 更新整体配置时发生错误: {}", + e ); - // 恢复原始配置文件 - fs::write(&file_path, original_content) - .await - .stringify_err()?; - // 发送合并文件专用错误通知 - let result = (false, error_msg.clone()); - crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); - return Ok(()); - } - Err(e) => { - logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); - // 恢复原始配置文件 - fs::write(&file_path, original_content) - .await - .stringify_err()?; - return Err(e.to_string().into()); + } else { + handle::Handle::refresh_clash(); } + Ok(()) + } + Ok((false, error_msg)) => { + logging!( + warn, + Type::Config, + "[cmd配置save] merge文件语法验证失败: {}", + error_msg + ); + restore_original(file_path, original_content).await?; + let result = (false, error_msg.clone()); + crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); + Ok(()) + } + Err(e) => { + logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); + restore_original(file_path, original_content).await?; + Err(e.to_string().into()) } } +} - // 非merge文件使用完整验证流程 - match CoreConfigValidator::validate_config_file(&file_path_str, None).await { +async fn handle_full_validation( + file_path_str: &str, + file_path: &std::path::Path, + original_content: &str, +) -> CmdResult { + match CoreConfigValidator::validate_config_file(file_path_str, None).await { Ok((true, _)) => { logging!(info, Type::Config, "[cmd配置save] 验证成功"); Ok(()) } Ok((false, error_msg)) => { logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg); - // 恢复原始配置文件 - fs::write(&file_path, original_content) - .await - .stringify_err()?; - - // 智能判断错误类型 - let is_script_error = file_path_str.ends_with(".js") - || error_msg.contains("Script syntax error") - || error_msg.contains("Script must contain a main function") - || error_msg.contains("Failed to read script file"); + restore_original(file_path, original_content).await?; if error_msg.contains("YAML syntax error") || error_msg.contains("Failed to read file:") - || (!file_path_str.ends_with(".js") && !is_script_error) + || (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str)) { - // 普通YAML错误使用YAML通知处理 logging!( info, Type::Config, "[cmd配置save] YAML配置文件验证失败,发送通知" ); - let result = (false, error_msg.clone()); + let result = (false, error_msg.to_owned()); crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件"); - } else if is_script_error { - // 脚本错误使用专门的通知处理 + } else if is_script_error(&error_msg, file_path_str) { logging!( info, Type::Config, "[cmd配置save] 脚本文件验证失败,发送通知" ); - let result = (false, error_msg.clone()); + let result = (false, error_msg.to_owned()); crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); } else { - // 普通配置错误使用一般通知 logging!( info, Type::Config, @@ -150,10 +164,7 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR } Err(e) => { logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); - // 恢复原始配置文件 - fs::write(&file_path, original_content) - .await - .stringify_err()?; + restore_original(file_path, original_content).await?; Err(e.to_string().into()) } } diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 5231ea90..75d1a544 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -3,6 +3,7 @@ use crate::{ core::service::{self, SERVICE_MANAGER, ServiceStatus}, utils::i18n::t, }; +use smartstring::SmartString; async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult { if let Err(e) = SERVICE_MANAGER @@ -12,7 +13,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> .await { let emsg = format!("{} Service failed: {}", op_type, e); - return Err(t(emsg.as_str()).await.into()); + return Err(SmartString::from(&*t(emsg.as_str()).await)); } Ok(()) } diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index eb74339f..cce4043d 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -9,12 +9,12 @@ pub async fn get_verge_config() -> CmdResult { let ref_data = verge.latest_ref(); ref_data.clone() }; - let verge_response = IVergeResponse::from(*verge_data); + let verge_response = IVergeResponse::from(verge_data); Ok(verge_response) } /// 修改Verge配置 #[tauri::command] pub async fn patch_verge_config(payload: IVerge) -> CmdResult { - feat::patch_verge(payload, false).await.stringify_err() + feat::patch_verge(&payload, false).await.stringify_err() } diff --git a/src-tauri/src/cmd/webdav.rs b/src-tauri/src/cmd/webdav.rs index 6c27868d..1de27b70 100644 --- a/src-tauri/src/cmd/webdav.rs +++ b/src-tauri/src/cmd/webdav.rs @@ -12,10 +12,7 @@ pub async fn save_webdav_config(url: String, username: String, password: String) webdav_password: Some(password), ..IVerge::default() }; - Config::verge() - .await - .draft_mut() - .patch_config(patch.clone()); + Config::verge().await.draft_mut().patch_config(&patch); Config::verge().await.apply(); // 分离数据获取和异步调用 diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index ac216933..8b1c072a 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -1,6 +1,8 @@ use crate::config::Config; +use crate::constants::{network, tun as tun_const}; use crate::utils::dirs::{ipc_path, path_to_str}; use crate::utils::{dirs, help}; +use crate::{logging, utils::logging::Type}; use anyhow::Result; use serde::{Deserialize, Serialize}; use serde_yaml_ng::{Mapping, Value}; @@ -40,15 +42,13 @@ impl IClashTemp { Self(Self::guard(map)) } Err(err) => { - log::error!(target: "app", "{err}"); + logging!(error, Type::Config, "{err}"); template } } } pub fn template() -> Self { - use crate::constants::{network, tun as tun_const}; - let mut map = Mapping::new(); let mut tun_config = Mapping::new(); let mut cors_map = Mapping::new(); @@ -214,9 +214,9 @@ impl IClashTemp { Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) - .unwrap_or(7896); + .unwrap_or(network::ports::DEFAULT_TPROXY); if port == 0 { - port = 7896; + port = network::ports::DEFAULT_TPROXY; } port } @@ -330,7 +330,7 @@ impl IClashTemp { .ok() .and_then(|path| path_to_str(&path).ok().map(|s| s.into())) .unwrap_or_else(|| { - log::error!(target: "app", "Failed to get IPC path"); + logging!(error, Type::Config, "Failed to get IPC path"); crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into() }) } diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index ced80e1d..f9bbcf0a 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -1,9 +1,10 @@ use super::{IClashTemp, IProfiles, IRuntime, IVerge}; use crate::{ + cmd, config::{PrfItem, profiles_append_item_safe}, constants::{files, timing}, - core::{CoreManager, handle, validate::CoreConfigValidator}, - enhance, logging, + core::{CoreManager, handle, service, tray, validate::CoreConfigValidator}, + enhance, logging, logging_error, utils::{Draft, dirs, help, logging::Type}, }; use anyhow::{Result, anyhow}; @@ -53,24 +54,47 @@ impl Config { /// 初始化订阅 pub async fn init_config() -> Result<()> { - if Self::profiles() - .await - .latest_ref() - .get_item(&"Merge".into()) - .is_err() + Self::ensure_default_profile_items().await?; + + // init Tun mode + if !cmd::system::is_admin().unwrap_or_default() + && service::is_service_available().await.is_err() { - let merge_item = PrfItem::from_merge(Some("Merge".into()))?; - profiles_append_item_safe(merge_item.clone()).await?; + let verge = Config::verge().await; + verge.draft_mut().enable_tun_mode = Some(false); + verge.apply(); + let _ = tray::Tray::global().update_tray_display().await; + + // 分离数据获取和异步调用避免Send问题 + let verge_data = Config::verge().await.latest_ref().clone(); + logging_error!(Type::Core, verge_data.save_file().await); } - if Self::profiles() - .await - .latest_ref() - .get_item(&"Script".into()) - .is_err() - { - let script_item = PrfItem::from_script(Some("Script".into()))?; - profiles_append_item_safe(script_item.clone()).await?; + + let validation_result = Self::generate_and_validate().await?; + + if let Some((msg_type, msg_content)) = validation_result { + sleep(timing::STARTUP_ERROR_DELAY).await; + handle::Handle::notice_message(msg_type, msg_content); } + + Ok(()) + } + + // Ensure "Merge" and "Script" profile items exist, adding them if missing. + async fn ensure_default_profile_items() -> Result<()> { + let profiles = Self::profiles().await; + if profiles.latest_ref().get_item("Merge").is_err() { + let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?; + profiles_append_item_safe(merge_item).await?; + } + if profiles.latest_ref().get_item("Script").is_err() { + let script_item = &mut PrfItem::from_script(Some("Script".into()))?; + profiles_append_item_safe(script_item).await?; + } + Ok(()) + } + + async fn generate_and_validate() -> Result> { // 生成运行时配置 if let Err(err) = Self::generate().await { logging!(error, Type::Config, "生成运行时配置失败: {}", err); @@ -81,7 +105,7 @@ impl Config { // 生成运行时配置文件并验证 let config_result = Self::generate_file(ConfigType::Run).await; - let validation_result = if config_result.is_ok() { + if config_result.is_ok() { // 验证配置文件 logging!(info, Type::Config, "开始验证配置"); @@ -97,12 +121,12 @@ impl Config { CoreManager::global() .use_default_config("config_validate::boot_error", &error_msg) .await?; - Some(("config_validate::boot_error", error_msg)) + Ok(Some(("config_validate::boot_error", error_msg))) } else { logging!(info, Type::Config, "配置验证成功"); // 前端没有必要知道验证成功的消息,也没有事件驱动 // Some(("config_validate::success", String::new())) - None + Ok(None) } } Err(err) => { @@ -110,7 +134,7 @@ impl Config { CoreManager::global() .use_default_config("config_validate::process_terminated", "") .await?; - Some(("config_validate::process_terminated", String::new())) + Ok(Some(("config_validate::process_terminated", String::new()))) } } } else { @@ -118,15 +142,8 @@ impl Config { CoreManager::global() .use_default_config("config_validate::error", "") .await?; - Some(("config_validate::error", String::new())) - }; - - if let Some((msg_type, msg_content)) = validation_result { - sleep(timing::STARTUP_ERROR_DELAY).await; - handle::Handle::notice_message(msg_type, msg_content); + Ok(Some(("config_validate::error", String::new()))) } - - Ok(()) } pub async fn generate_file(typ: ConfigType) -> Result { @@ -151,11 +168,11 @@ impl Config { pub async fn generate() -> Result<()> { let (config, exists_keys, logs) = enhance::enhance().await; - *Config::runtime().await.draft_mut() = Box::new(IRuntime { + **Config::runtime().await.draft_mut() = IRuntime { config: Some(config), exists_keys, chain_logs: logs, - }); + }; Ok(()) } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index 7475cf1f..d6b54b56 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -1,13 +1,17 @@ -use crate::utils::{ - dirs, help, - network::{NetworkManager, ProxyType}, - tmpl, +use crate::{ + config::profiles, + utils::{ + dirs, help, + network::{NetworkManager, ProxyType}, + tmpl, + }, }; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; -use std::{fs, time::Duration}; +use std::time::Duration; +use tokio::fs; #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct PrfItem { @@ -118,26 +122,29 @@ pub struct PrfOption { } impl PrfOption { - pub fn merge(one: Option, other: Option) -> Option { + pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option { match (one, other) { - (Some(mut a), Some(b)) => { - a.user_agent = b.user_agent.or(a.user_agent); - a.with_proxy = b.with_proxy.or(a.with_proxy); - a.self_proxy = b.self_proxy.or(a.self_proxy); - a.danger_accept_invalid_certs = b + (Some(a_ref), Some(b_ref)) => { + let mut result = a_ref.clone(); + result.user_agent = b_ref.user_agent.clone().or(result.user_agent); + result.with_proxy = b_ref.with_proxy.or(result.with_proxy); + result.self_proxy = b_ref.self_proxy.or(result.self_proxy); + result.danger_accept_invalid_certs = b_ref .danger_accept_invalid_certs - .or(a.danger_accept_invalid_certs); - a.allow_auto_update = b.allow_auto_update.or(a.allow_auto_update); - a.update_interval = b.update_interval.or(a.update_interval); - a.merge = b.merge.or(a.merge); - a.script = b.script.or(a.script); - a.rules = b.rules.or(a.rules); - a.proxies = b.proxies.or(a.proxies); - a.groups = b.groups.or(a.groups); - a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds); - Some(a) + .or(result.danger_accept_invalid_certs); + result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); + result.update_interval = b_ref.update_interval.or(result.update_interval); + result.merge = b_ref.merge.clone().or(result.merge); + result.script = b_ref.script.clone().or(result.script); + result.rules = b_ref.rules.clone().or(result.rules); + result.proxies = b_ref.proxies.clone().or(result.proxies); + result.groups = b_ref.groups.clone().or(result.groups); + result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds); + Some(result) } - t => t.0.or(t.1), + (Some(a_ref), None) => Some(a_ref.clone()), + (None, Some(b_ref)) => Some(b_ref.clone()), + (None, None) => None, } } } @@ -145,13 +152,14 @@ impl PrfOption { impl PrfItem { /// From partial item /// must contain `itype` - pub async fn from(item: PrfItem, file_data: Option) -> Result { + pub async fn from(item: &PrfItem, file_data: Option) -> Result { if item.itype.is_none() { bail!("type should not be null"); } let itype = item .itype + .as_ref() .ok_or_else(|| anyhow::anyhow!("type should not be null"))?; match itype.as_str() { "remote" => { @@ -159,14 +167,16 @@ impl PrfItem { .url .as_ref() .ok_or_else(|| anyhow::anyhow!("url should not be null"))?; - let name = item.name; - let desc = item.desc; - PrfItem::from_url(url, name, desc, item.option).await + let name = item.name.as_ref(); + let desc = item.desc.as_ref(); + let option = item.option.as_ref(); + PrfItem::from_url(url, name, desc, option).await } "local" => { - let name = item.name.unwrap_or_else(|| "Local File".into()); - let desc = item.desc.unwrap_or_else(|| "".into()); - PrfItem::from_local(name, desc, file_data, item.option).await + let name = item.name.clone().unwrap_or_else(|| "Local File".into()); + let desc = item.desc.clone().unwrap_or_else(|| "".into()); + let option = item.option.as_ref(); + PrfItem::from_local(name, desc, file_data, option).await } typ => bail!("invalid profile item type \"{typ}\""), } @@ -178,7 +188,7 @@ impl PrfItem { name: String, desc: String, file_data: Option, - option: Option, + option: Option<&PrfOption>, ) -> Result { let uid = help::get_uid("L").into(); let file = format!("{uid}.yaml").into(); @@ -191,29 +201,29 @@ impl PrfItem { let mut groups = opt_ref.and_then(|o| o.groups.clone()); if merge.is_none() { - let merge_item = PrfItem::from_merge(None)?; - crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?; - merge = merge_item.uid; + let merge_item = &mut PrfItem::from_merge(None)?; + profiles::profiles_append_item_safe(merge_item).await?; + merge = merge_item.uid.clone(); } if script.is_none() { - let script_item = PrfItem::from_script(None)?; - crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?; - script = script_item.uid; + let script_item = &mut PrfItem::from_script(None)?; + profiles::profiles_append_item_safe(script_item).await?; + script = script_item.uid.clone(); } if rules.is_none() { - let rules_item = PrfItem::from_rules()?; - crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?; - rules = rules_item.uid; + let rules_item = &mut PrfItem::from_rules()?; + profiles::profiles_append_item_safe(rules_item).await?; + rules = rules_item.uid.clone(); } if proxies.is_none() { - let proxies_item = PrfItem::from_proxies()?; - crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?; - proxies = proxies_item.uid; + let proxies_item = &mut PrfItem::from_proxies()?; + profiles::profiles_append_item_safe(proxies_item).await?; + proxies = proxies_item.uid.clone(); } if groups.is_none() { - let groups_item = PrfItem::from_groups()?; - crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?; - groups = groups_item.uid; + let groups_item = &mut PrfItem::from_groups()?; + profiles::profiles_append_item_safe(groups_item).await?; + groups = groups_item.uid.clone(); } Ok(PrfItem { uid: Some(uid), @@ -243,24 +253,23 @@ impl PrfItem { /// create a new item from url pub async fn from_url( url: &str, - name: Option, - desc: Option, - option: Option, + name: Option<&String>, + desc: Option<&String>, + option: Option<&PrfOption>, ) -> Result { - let opt_ref = option.as_ref(); - let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false)); - let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false)); + let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false)); + let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false)); let accept_invalid_certs = - opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false)); - let allow_auto_update = opt_ref.map(|o| o.allow_auto_update.unwrap_or(true)); - let user_agent = opt_ref.and_then(|o| o.user_agent.clone()); - let update_interval = opt_ref.and_then(|o| o.update_interval); - let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20); - let mut merge = opt_ref.and_then(|o| o.merge.clone()); - let mut script = opt_ref.and_then(|o| o.script.clone()); - let mut rules = opt_ref.and_then(|o| o.rules.clone()); - let mut proxies = opt_ref.and_then(|o| o.proxies.clone()); - let mut groups = opt_ref.and_then(|o| o.groups.clone()); + option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false)); + let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true)); + let user_agent = option.and_then(|o| o.user_agent.clone()); + let update_interval = option.and_then(|o| o.update_interval); + let timeout = option.and_then(|o| o.timeout_seconds).unwrap_or(20); + let mut merge = option.and_then(|o| o.merge.clone()); + let mut script = option.and_then(|o| o.script.clone()); + let mut rules = option.and_then(|o| o.rules.clone()); + let mut proxies = option.and_then(|o| o.proxies.clone()); + let mut groups = option.and_then(|o| o.groups.clone()); // 选择代理类型 let proxy_type = if self_proxy { @@ -297,18 +306,27 @@ impl PrfItem { let header = resp.headers(); // parse the Subscription UserInfo - let extra = match header.get("Subscription-Userinfo") { - Some(value) => { - let sub_info = value.to_str().unwrap_or(""); - Some(PrfExtra { - upload: help::parse_str(sub_info, "upload").unwrap_or(0), - download: help::parse_str(sub_info, "download").unwrap_or(0), - total: help::parse_str(sub_info, "total").unwrap_or(0), - expire: help::parse_str(sub_info, "expire").unwrap_or(0), - }) + let extra; + 'extra: { + for (k, v) in header.iter() { + let key_lower = k.as_str().to_ascii_lowercase(); + // Accept standard custom-metadata prefixes (x-amz-meta-, x-obs-meta-, x-cos-meta-, etc.). + if key_lower + .strip_suffix("subscription-userinfo") + .is_some_and(|prefix| prefix.is_empty() || prefix.ends_with('-')) + { + let sub_info = v.to_str().unwrap_or(""); + extra = Some(PrfExtra { + upload: help::parse_str(sub_info, "upload").unwrap_or(0), + download: help::parse_str(sub_info, "download").unwrap_or(0), + total: help::parse_str(sub_info, "total").unwrap_or(0), + expire: help::parse_str(sub_info, "expire").unwrap_or(0), + }); + break 'extra; + } } - None => None, - }; + extra = None; + } // parse the Content-Disposition let filename = match header.get("Content-Disposition") { @@ -356,7 +374,11 @@ impl PrfItem { let uid = help::get_uid("R").into(); let file = format!("{uid}.yaml").into(); - let name = name.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()).into()); + let name = name.map(|s| s.to_owned()).unwrap_or_else(|| { + filename + .map(|s| s.into()) + .unwrap_or_else(|| "Remote File".into()) + }); let data = resp.text_with_charset()?; // process the charset "UTF-8 with BOM" @@ -371,36 +393,36 @@ impl PrfItem { } if merge.is_none() { - let merge_item = PrfItem::from_merge(None)?; - crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?; - merge = merge_item.uid; + let merge_item = &mut PrfItem::from_merge(None)?; + profiles::profiles_append_item_safe(merge_item).await?; + merge = merge_item.uid.clone(); } if script.is_none() { - let script_item = PrfItem::from_script(None)?; - crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?; - script = script_item.uid; + let script_item = &mut PrfItem::from_script(None)?; + profiles::profiles_append_item_safe(script_item).await?; + script = script_item.uid.clone(); } if rules.is_none() { - let rules_item = PrfItem::from_rules()?; - crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?; - rules = rules_item.uid; + let rules_item = &mut PrfItem::from_rules()?; + profiles::profiles_append_item_safe(rules_item).await?; + rules = rules_item.uid.clone(); } if proxies.is_none() { - let proxies_item = PrfItem::from_proxies()?; - crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?; - proxies = proxies_item.uid; + let proxies_item = &mut PrfItem::from_proxies()?; + profiles::profiles_append_item_safe(proxies_item).await?; + proxies = proxies_item.uid.clone(); } if groups.is_none() { - let groups_item = PrfItem::from_groups()?; - crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?; - groups = groups_item.uid; + let groups_item = &mut PrfItem::from_groups()?; + profiles::profiles_append_item_safe(groups_item).await?; + groups = groups_item.uid.clone(); } Ok(PrfItem { uid: Some(uid), itype: Some("remote".into()), name: Some(name), - desc, + desc: desc.cloned(), file: Some(file), url: Some(url.into()), selected: None, @@ -537,24 +559,28 @@ impl PrfItem { } /// get the file data - pub fn read_file(&self) -> Result { + pub async fn read_file(&self) -> Result { let file = self .file - .clone() + .as_ref() .ok_or_else(|| anyhow::anyhow!("could not find the file"))?; let path = dirs::app_profiles_dir()?.join(file.as_str()); - let content = fs::read_to_string(path).context("failed to read the file")?; + let content = fs::read_to_string(path) + .await + .context("failed to read the file")?; Ok(content.into()) } /// save the file data - pub fn save_file(&self, data: String) -> Result<()> { + pub async fn save_file(&self, data: String) -> Result<()> { let file = self .file - .clone() + .as_ref() .ok_or_else(|| anyhow::anyhow!("could not find the file"))?; let path = dirs::app_profiles_dir()?.join(file.as_str()); - fs::write(path, data.as_bytes()).context("failed to save the file") + fs::write(path, data.as_bytes()) + .await + .context("failed to save the file") } } diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 776ee21a..f25e5990 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -3,6 +3,7 @@ use crate::utils::{ dirs::{self, PathBufExec}, help, }; +use crate::{logging, utils::logging::Type}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; @@ -31,12 +32,24 @@ pub struct CleanupResult { macro_rules! patch { ($lv: expr, $rv: expr, $key: tt) => { if ($rv.$key).is_some() { - $lv.$key = $rv.$key; + $lv.$key = $rv.$key.clone(); } }; } impl IProfiles { + // Helper to find and remove an item by uid from the items vec, returning its file name (if any). + fn take_item_file_by_uid( + items: &mut Vec, + target_uid: Option, + ) -> Option { + for (i, _) in items.iter().enumerate() { + if items[i].uid == target_uid { + return items.remove(i).file; + } + } + None + } pub async fn new() -> Self { match dirs::profiles_path() { Ok(path) => match help::read_yaml::(&path).await { @@ -55,12 +68,12 @@ impl IProfiles { profiles } Err(err) => { - log::error!(target: "app", "{err}"); + logging!(error, Type::Config, "{err}"); Self::template() } }, Err(err) => { - log::error!(target: "app", "{err}"); + logging!(error, Type::Config, "{err}"); Self::template() } } @@ -110,28 +123,30 @@ impl IProfiles { } /// find the item by the uid - pub fn get_item(&self, uid: &String) -> Result<&PrfItem> { - if let Some(items) = self.items.as_ref() { - let some_uid = Some(uid.clone()); + pub fn get_item(&self, uid: impl AsRef) -> Result<&PrfItem> { + let uid_str = uid.as_ref(); + if let Some(items) = self.items.as_ref() { for each in items.iter() { - if each.uid == some_uid { + if let Some(uid_val) = &each.uid + && uid_val.as_str() == uid_str + { return Ok(each); } } } - bail!("failed to get the profile item \"uid:{uid}\""); + bail!("failed to get the profile item \"uid:{}\"", uid_str); } /// append new item /// if the file_data is some /// then should save the data to file - pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> { - if item.uid.is_none() { + pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> { + let uid = &item.uid; + if uid.is_none() { bail!("the uid should not be null"); } - let uid = item.uid.clone(); // save the file data // move the field value after save @@ -153,7 +168,7 @@ impl IProfiles { if self.current.is_none() && (item.itype == Some("remote".into()) || item.itype == Some("local".into())) { - self.current = uid; + self.current = uid.to_owned(); } if self.items.is_none() { @@ -161,24 +176,23 @@ impl IProfiles { } if let Some(items) = self.items.as_mut() { - items.push(item) + items.push(item.to_owned()); } - // self.save_file().await Ok(()) } /// reorder items - pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> { + pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> { let mut items = self.items.take().unwrap_or_default(); let mut old_index = None; let mut new_index = None; for (i, _) in items.iter().enumerate() { - if items[i].uid == Some(active_id.clone()) { + if items[i].uid.as_ref() == Some(active_id) { old_index = Some(i); } - if items[i].uid == Some(over_id.clone()) { + if items[i].uid.as_ref() == Some(over_id) { new_index = Some(i); } } @@ -194,11 +208,11 @@ impl IProfiles { } /// update the item value - pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { + pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> { let mut items = self.items.take().unwrap_or_default(); for each in items.iter_mut() { - if each.uid == Some(uid.clone()) { + if each.uid.as_ref() == Some(uid) { patch!(each, item, itype); patch!(each, item, name); patch!(each, item, desc); @@ -220,13 +234,13 @@ impl IProfiles { /// be used to update the remote item /// only patch `updated` `extra` `file_data` - pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> { + pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> { if self.items.is_none() { self.items = Some(vec![]); } // find the item - let _ = self.get_item(&uid)?; + let _ = self.get_item(uid)?; if let Some(items) = self.items.as_mut() { let some_uid = Some(uid.clone()); @@ -235,8 +249,8 @@ impl IProfiles { if each.uid == some_uid { each.extra = item.extra; each.updated = item.updated; - each.home = item.home; - each.option = PrfOption::merge(each.option.clone(), item.option); + each.home = item.home.to_owned(); + each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref()); // save the file data // move the field value after save if let Some(file_data) = item.file_data.take() { @@ -267,115 +281,58 @@ impl IProfiles { /// delete item /// if delete the current then return true - pub async fn delete_item(&mut self, uid: String) -> Result { - let current = self.current.as_ref().unwrap_or(&uid); + pub async fn delete_item(&mut self, uid: &String) -> Result { + let current = self.current.as_ref().unwrap_or(uid); let current = current.clone(); - let item = self.get_item(&uid)?; + let item = self.get_item(uid)?; let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone()); let script_uid = item.option.as_ref().and_then(|e| e.script.clone()); let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone()); let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone()); let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone()); let mut items = self.items.take().unwrap_or_default(); - let mut index = None; - let mut merge_index = None; - let mut script_index = None; - let mut rules_index = None; - let mut proxies_index = None; - let mut groups_index = None; - // get the index - for (i, _) in items.iter().enumerate() { - if items[i].uid == Some(uid.clone()) { - index = Some(i); - break; - } - } - if let Some(index) = index - && let Some(file) = items.remove(index).file - { + // remove the main item (if exists) and delete its file + if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) { let _ = dirs::app_profiles_dir()? .join(file.as_str()) .remove_if_exists() .await; } - // get the merge index - for (i, _) in items.iter().enumerate() { - if items[i].uid == merge_uid { - merge_index = Some(i); - break; - } - } - if let Some(index) = merge_index - && let Some(file) = items.remove(index).file - { + + // remove related extension items (merge, script, rules, proxies, groups) + if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) { let _ = dirs::app_profiles_dir()? .join(file.as_str()) .remove_if_exists() .await; } - // get the script index - for (i, _) in items.iter().enumerate() { - if items[i].uid == script_uid { - script_index = Some(i); - break; - } - } - if let Some(index) = script_index - && let Some(file) = items.remove(index).file - { + if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) { let _ = dirs::app_profiles_dir()? .join(file.as_str()) .remove_if_exists() .await; } - // get the rules index - for (i, _) in items.iter().enumerate() { - if items[i].uid == rules_uid { - rules_index = Some(i); - break; - } - } - if let Some(index) = rules_index - && let Some(file) = items.remove(index).file - { + if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) { let _ = dirs::app_profiles_dir()? .join(file.as_str()) .remove_if_exists() .await; } - // get the proxies index - for (i, _) in items.iter().enumerate() { - if items[i].uid == proxies_uid { - proxies_index = Some(i); - break; - } - } - if let Some(index) = proxies_index - && let Some(file) = items.remove(index).file - { + if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) { let _ = dirs::app_profiles_dir()? .join(file.as_str()) .remove_if_exists() .await; } - // get the groups index - for (i, _) in items.iter().enumerate() { - if items[i].uid == groups_uid { - groups_index = Some(i); - break; - } - } - if let Some(index) = groups_index - && let Some(file) = items.remove(index).file - { + if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) { let _ = dirs::app_profiles_dir()? .join(file.as_str()) .remove_if_exists() .await; } // delete the original uid - if current == uid { + if current == *uid { self.current = None; for item in items.iter() { if item.itype == Some("remote".into()) || item.itype == Some("local".into()) { @@ -387,7 +344,7 @@ impl IProfiles { self.items = Some(items); self.save_file().await?; - Ok(current == uid) + Ok(current == *uid) } /// 获取current指向的订阅内容 @@ -478,8 +435,8 @@ impl IProfiles { } /// 判断profile是否是current指向的 - pub fn is_current_profile_index(&self, index: String) -> bool { - self.current == Some(index) + pub fn is_current_profile_index(&self, index: &String) -> bool { + self.current.as_ref() == Some(index) } /// 获取所有的profiles(uid,名称) @@ -498,6 +455,18 @@ impl IProfiles { }) } + /// 通过 uid 获取名称 + pub fn get_name_by_uid(&self, uid: &String) -> Option { + if let Some(items) = &self.items { + for item in items { + if item.uid.as_ref() == Some(uid) { + return item.name.clone(); + } + } + } + None + } + /// 以 app 中的 profile 列表为准,删除不再需要的文件 pub async fn cleanup_orphaned_files(&self) -> Result { let profiles_dir = dirs::app_profiles_dir()?; @@ -536,7 +505,7 @@ impl IProfiles { { // 检查是否为全局扩展文件 if protected_files.contains(file_name) { - log::debug!(target: "app", "保护全局扩展配置文件: {file_name}"); + logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}"); continue; } @@ -545,11 +514,15 @@ impl IProfiles { match path.to_path_buf().remove_if_exists().await { Ok(_) => { deleted_files.push(file_name.into()); - log::info!(target: "app", "已清理冗余文件: {file_name}"); + logging!(info, Type::Config, "已清理冗余文件: {file_name}"); } Err(e) => { failed_deletions.push(format!("{file_name}: {e}").into()); - log::warn!(target: "app", "清理文件失败: {file_name} - {e}"); + logging!( + warn, + Type::Config, + "Warning: 清理文件失败: {file_name} - {e}" + ); } } } @@ -562,8 +535,9 @@ impl IProfiles { failed_deletions, }; - log::info!( - target: "app", + logging!( + info, + Type::Config, "Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}", result.total_files, result.deleted_files.len(), @@ -671,14 +645,14 @@ impl IProfiles { use crate::config::Config; pub async fn profiles_append_item_with_filedata_safe( - item: PrfItem, + item: &PrfItem, file_data: Option, ) -> Result<()> { - let item = PrfItem::from(item, file_data).await?; + let item = &mut PrfItem::from(item, file_data).await?; profiles_append_item_safe(item).await } -pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> { +pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -688,7 +662,7 @@ pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> { .await } -pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> { +pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -698,7 +672,7 @@ pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<() .await } -pub async fn profiles_delete_item_safe(index: String) -> Result { +pub async fn profiles_delete_item_safe(index: &String) -> Result { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -708,7 +682,7 @@ pub async fn profiles_delete_item_safe(index: String) -> Result { .await } -pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> { +pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { @@ -728,7 +702,7 @@ pub async fn profiles_save_file_safe() -> Result<()> { .await } -pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> { +pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> { Config::profiles() .await .with_data_modify(|mut profiles| async move { diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 8533231e..1e37cee4 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::{ config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, logging, @@ -203,8 +204,7 @@ pub struct IVerge { pub enable_tray_speed: Option, - pub enable_tray_icon: Option, - + // pub enable_tray_icon: Option, /// show proxy groups directly on tray root menu pub tray_inline_proxy_groups: Option, @@ -305,19 +305,17 @@ impl IVerge { /// 配置修正后重新加载配置 async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> { - use crate::config::Config; - - let config_draft = Config::verge().await; - *config_draft.draft_mut() = Box::new(updated_config.clone()); - config_draft.apply(); - logging!( info, Type::Config, "内存配置已强制更新,新的clash_core: {:?}", - updated_config.clash_core + &updated_config.clash_core ); + let config_draft = Config::verge().await; + **config_draft.draft_mut() = updated_config; + config_draft.apply(); + Ok(()) } @@ -355,12 +353,12 @@ impl IVerge { config } Err(err) => { - log::error!(target: "app", "{err}"); + logging!(error, Type::Config, "{err}"); Self::template() } }, Err(err) => { - log::error!(target: "app", "{err}"); + logging!(error, Type::Config, "{err}"); Self::template() } } @@ -419,7 +417,7 @@ impl IVerge { webdav_username: None, webdav_password: None, enable_tray_speed: Some(false), - enable_tray_icon: Some(true), + // enable_tray_icon: Some(true), tray_inline_proxy_groups: Some(false), enable_global_hotkey: Some(true), enable_auto_light_weight_mode: Some(false), @@ -438,11 +436,12 @@ impl IVerge { /// patch verge config /// only save to file - pub fn patch_config(&mut self, patch: IVerge) { + #[allow(clippy::cognitive_complexity)] + pub fn patch_config(&mut self, patch: &IVerge) { macro_rules! patch { ($key: tt) => { if patch.$key.is_some() { - self.$key = patch.$key; + self.$key = patch.$key.clone(); } }; } @@ -514,7 +513,7 @@ impl IVerge { patch!(webdav_username); patch!(webdav_password); patch!(enable_tray_speed); - patch!(enable_tray_icon); + // patch!(enable_tray_icon); patch!(tray_inline_proxy_groups); patch!(enable_auto_light_weight_mode); patch!(auto_light_weight_minutes); @@ -608,7 +607,7 @@ pub struct IVergeResponse { pub webdav_username: Option, pub webdav_password: Option, pub enable_tray_speed: Option, - pub enable_tray_icon: Option, + // pub enable_tray_icon: Option, pub tray_inline_proxy_groups: Option, pub enable_auto_light_weight_mode: Option, pub auto_light_weight_minutes: Option, @@ -685,7 +684,7 @@ impl From for IVergeResponse { webdav_username: verge.webdav_username, webdav_password: verge.webdav_password, enable_tray_speed: verge.enable_tray_speed, - enable_tray_icon: verge.enable_tray_icon, + // enable_tray_icon: verge.enable_tray_icon, tray_inline_proxy_groups: verge.tray_inline_proxy_groups, enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode, auto_light_weight_minutes: verge.auto_light_weight_minutes, @@ -697,3 +696,9 @@ impl From for IVergeResponse { } } } + +impl From> for IVergeResponse { + fn from(verge: Box) -> Self { + IVergeResponse::from(*verge) + } +} diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index e3ea41fd..3195ea40 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -5,15 +5,13 @@ pub mod network { pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097"; pub mod ports { - #[allow(dead_code)] + #[cfg(not(target_os = "windows"))] pub const DEFAULT_REDIR: u16 = 7895; - #[allow(dead_code)] + #[cfg(target_os = "linux")] pub const DEFAULT_TPROXY: u16 = 7896; pub const DEFAULT_MIXED: u16 = 7897; pub const DEFAULT_SOCKS: u16 = 7898; pub const DEFAULT_HTTP: u16 = 7899; - #[allow(dead_code)] - pub const DEFAULT_EXTERNAL_CONTROLLER: u16 = 9097; #[cfg(not(feature = "verge-dev"))] pub const SINGLETON_SERVER: u16 = 33331; @@ -39,11 +37,8 @@ pub mod timing { pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500); pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300); - pub const PROCESS_VERIFY_DELAY: Duration = Duration::from_millis(100); - #[allow(dead_code)] pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20); pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2); - #[allow(dead_code)] pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300); #[cfg(target_os = "windows")] @@ -53,40 +48,16 @@ pub mod timing { } pub mod retry { - #[allow(dead_code)] pub const EVENT_EMIT_THRESHOLD: u64 = 10; - #[allow(dead_code)] - pub const SWR_ERROR_RETRY: usize = 2; } pub mod files { pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; - #[allow(dead_code)] pub const DNS_CONFIG: &str = "dns_config.yaml"; - #[allow(dead_code)] pub const WINDOW_STATE: &str = "window_state.json"; } -pub mod process { - pub const VERGE_MIHOMO: &str = "verge-mihomo"; - pub const VERGE_MIHOMO_ALPHA: &str = "verge-mihomo-alpha"; - - pub fn process_names() -> [&'static str; 2] { - [VERGE_MIHOMO, VERGE_MIHOMO_ALPHA] - } - - #[cfg(windows)] - pub fn with_extension(name: &str) -> String { - format!("{}.exe", name) - } - - #[cfg(not(windows))] - pub fn with_extension(name: &str) -> String { - name.to_string() - } -} - pub mod error_patterns { pub const CONNECTION_ERRORS: &[&str] = &[ "Failed to create connection", diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs index 2fd8b3d8..c9e31c67 100644 --- a/src-tauri/src/core/async_proxy_query.rs +++ b/src-tauri/src/core/async_proxy_query.rs @@ -1,5 +1,6 @@ #[cfg(target_os = "windows")] use crate::process::AsyncHandler; +use crate::{logging, utils::logging::Type}; use anyhow::Result; use serde::{Deserialize, Serialize}; use tokio::time::{Duration, timeout}; @@ -41,15 +42,21 @@ impl AsyncProxyQuery { pub async fn get_auto_proxy() -> AsyncAutoproxy { match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await { Ok(Ok(proxy)) => { - log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url); + logging!( + debug, + Type::Network, + "异步获取自动代理成功: enable={}, url={}", + proxy.enable, + proxy.url + ); proxy } Ok(Err(e)) => { - log::warn!(target: "app", "异步获取自动代理失败: {e}"); + logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}"); AsyncAutoproxy::default() } Err(_) => { - log::warn!(target: "app", "异步获取自动代理超时"); + logging!(warn, Type::Network, "Warning: 异步获取自动代理超时"); AsyncAutoproxy::default() } } @@ -59,15 +66,22 @@ impl AsyncProxyQuery { pub async fn get_system_proxy() -> AsyncSysproxy { match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await { Ok(Ok(proxy)) => { - log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port); + logging!( + debug, + Type::Network, + "异步获取系统代理成功: enable={}, {}:{}", + proxy.enable, + proxy.host, + proxy.port + ); proxy } Ok(Err(e)) => { - log::warn!(target: "app", "异步获取系统代理失败: {e}"); + logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}"); AsyncSysproxy::default() } Err(_) => { - log::warn!(target: "app", "异步获取系统代理超时"); + logging!(warn, Type::Network, "Warning: 异步获取系统代理超时"); AsyncSysproxy::default() } } @@ -99,7 +113,7 @@ impl AsyncProxyQuery { RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); if result != 0 { - log::debug!(target: "app", "无法打开注册表项"); + logging!(debug, Type::Network, "无法打开注册表项"); return Ok(AsyncAutoproxy::default()); } @@ -125,7 +139,7 @@ impl AsyncProxyQuery { .position(|&x| x == 0) .unwrap_or(url_buffer.len()); pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]); - log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}"); + logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}"); } // 2. 检查自动检测设置是否启用 @@ -150,7 +164,11 @@ impl AsyncProxyQuery { || (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0); if pac_enabled { - log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}"); + logging!( + debug, + Type::Network, + "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}" + ); if pac_url.is_empty() && auto_detect != 0 { pac_url = "auto-detect".into(); @@ -161,7 +179,7 @@ impl AsyncProxyQuery { url: pac_url, }) } else { - log::debug!(target: "app", "PAC配置未启用"); + logging!(debug, Type::Network, "PAC配置未启用"); Ok(AsyncAutoproxy::default()) } } @@ -177,7 +195,11 @@ impl AsyncProxyQuery { } let stdout = String::from_utf8_lossy(&output.stdout); - log::debug!(target: "app", "scutil output: {stdout}"); + crate::logging!( + debug, + crate::utils::logging::Type::Network, + "scutil output: {stdout}" + ); let mut pac_enabled = false; let mut pac_url = String::new(); @@ -196,7 +218,11 @@ impl AsyncProxyQuery { } } - log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}"); + crate::logging!( + debug, + crate::utils::logging::Type::Network, + "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}" + ); Ok(AsyncAutoproxy { enable: pac_enabled && !pac_url.is_empty(), @@ -363,7 +389,11 @@ impl AsyncProxyQuery { (proxy_server, 8080) }; - log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}"); + logging!( + debug, + Type::Network, + "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}" + ); Ok(AsyncSysproxy { enable: true, @@ -386,7 +416,7 @@ impl AsyncProxyQuery { } let stdout = String::from_utf8_lossy(&output.stdout); - log::debug!(target: "app", "scutil proxy output: {stdout}"); + logging!(debug, Type::Network, "scutil proxy output: {stdout}"); let mut http_enabled = false; let mut http_host = String::new(); diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index d9e46c68..cf90bdd1 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -1,4 +1,10 @@ -use crate::{config::Config, utils::dirs}; +use crate::constants::files::DNS_CONFIG; +use crate::{ + config::Config, + logging, + process::AsyncHandler, + utils::{dirs, logging::Type}, +}; use anyhow::Error; use once_cell::sync::OnceCell; use parking_lot::Mutex; @@ -7,13 +13,12 @@ use smartstring::alias::String; use std::{ collections::HashMap, env::{consts::OS, temp_dir}, - fs, io::Write, path::PathBuf, sync::Arc, time::Duration, }; -use tokio::time::timeout; +use tokio::{fs, time::timeout}; use zip::write::SimpleFileOptions; // 应用版本常量,来自 tauri.conf.json @@ -138,9 +143,14 @@ impl WebDavClient { .is_err() { match client.mkcol(dirs::BACKUP_DIR).await { - Ok(_) => log::info!("Successfully created backup directory"), + Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"), Err(e) => { - log::warn!("Failed to create backup directory: {}", e); + logging!( + warn, + Type::Backup, + "Warning: Failed to create backup directory: {}", + e + ); // 清除缓存,强制下次重新尝试 self.reset(); return Err(anyhow::Error::msg(format!( @@ -170,7 +180,7 @@ impl WebDavClient { let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into(); // 读取文件并上传,如果失败尝试一次重试 - let file_content = fs::read(&file_path)?; + let file_content = fs::read(&file_path).await?; // 添加超时保护 let upload_result = timeout( @@ -181,7 +191,11 @@ impl WebDavClient { match upload_result { Err(_) => { - log::warn!("Upload timed out, retrying once"); + logging!( + warn, + Type::Backup, + "Warning: Upload timed out, retrying once" + ); tokio::time::sleep(Duration::from_millis(500)).await; timeout( Duration::from_secs(TIMEOUT_UPLOAD), @@ -192,7 +206,11 @@ impl WebDavClient { } Ok(Err(e)) => { - log::warn!("Upload failed, retrying once: {e}"); + logging!( + warn, + Type::Backup, + "Warning: Upload failed, retrying once: {e}" + ); tokio::time::sleep(Duration::from_millis(500)).await; timeout( Duration::from_secs(TIMEOUT_UPLOAD), @@ -212,7 +230,7 @@ impl WebDavClient { let fut = async { let response = client.get(path.as_str()).await?; let content = response.bytes().await?; - fs::write(&storage_path, &content)?; + fs::write(&storage_path, &content).await?; Ok::<(), Error>(()) }; @@ -250,18 +268,19 @@ impl WebDavClient { } } -pub fn create_backup() -> Result<(String, PathBuf), Error> { +pub async fn create_backup() -> Result<(String, PathBuf), Error> { let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string(); let zip_file_name: String = format!("{OS}-backup-{now}.zip").into(); let zip_path = temp_dir().join(zip_file_name.as_str()); - let file = fs::File::create(&zip_path)?; + let value = zip_path.clone(); + let file = AsyncHandler::spawn_blocking(move || std::fs::File::create(&value)).await??; let mut zip = zip::ZipWriter::new(file); zip.add_directory("profiles/", SimpleFileOptions::default())?; let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); - if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) { - for entry in entries { - let entry = entry?; + + if let Ok(mut entries) = fs::read_dir(dirs::app_profiles_dir()?).await { + while let Some(entry) = entries.next_entry().await? { let path = entry.path(); if path.is_file() { let file_name_os = entry.file_name(); @@ -270,16 +289,16 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> { .ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?; let backup_path = format!("profiles/{}", file_name); zip.start_file(backup_path, options)?; - let file_content = fs::read(&path)?; + let file_content = fs::read(&path).await?; zip.write_all(&file_content)?; } } } zip.start_file(dirs::CLASH_CONFIG, options)?; - zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?; + zip.write_all(fs::read(dirs::clash_path()?).await?.as_slice())?; - let mut verge_config: serde_json::Value = - serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?; + let verge_text = fs::read_to_string(dirs::verge_path()?).await?; + let mut verge_config: serde_json::Value = serde_yaml_ng::from_str(&verge_text)?; if let Some(obj) = verge_config.as_object_mut() { obj.remove("webdav_username"); obj.remove("webdav_password"); @@ -288,14 +307,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> { zip.start_file(dirs::VERGE_CONFIG, options)?; zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?; - let dns_config_path = dirs::app_home_dir()?.join(dirs::DNS_CONFIG); + let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG); if dns_config_path.exists() { - zip.start_file(dirs::DNS_CONFIG, options)?; - zip.write_all(fs::read(&dns_config_path)?.as_slice())?; + zip.start_file(DNS_CONFIG, options)?; + zip.write_all(fs::read(&dns_config_path).await?.as_slice())?; } zip.start_file(dirs::PROFILE_YAML, options)?; - zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?; + zip.write_all(fs::read(dirs::profiles_path()?).await?.as_slice())?; zip.finish()?; Ok((zip_file_name, zip_path)) } diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index dd0928c7..18a6f155 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -7,6 +7,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use crate::config::{Config, IVerge}; use crate::core::{async_proxy_query::AsyncProxyQuery, handle}; use crate::process::AsyncHandler; +use crate::{logging, utils::logging::Type}; use once_cell::sync::Lazy; use smartstring::alias::String; use sysproxy::{Autoproxy, Sysproxy}; @@ -104,14 +105,14 @@ impl EventDrivenProxyManager { let query = QueryRequest { response_tx: tx }; if self.query_sender.send(query).is_err() { - log::error!(target: "app", "发送查询请求失败,返回缓存数据"); + logging!(error, Type::Network, "发送查询请求失败,返回缓存数据"); return self.get_auto_proxy_cached().await; } match timeout(Duration::from_secs(5), rx).await { Ok(Ok(result)) => result, _ => { - log::warn!(target: "app", "查询超时,返回缓存数据"); + logging!(warn, Type::Network, "Warning: 查询超时,返回缓存数据"); self.get_auto_proxy_cached().await } } @@ -134,7 +135,7 @@ impl EventDrivenProxyManager { fn send_event(&self, event: ProxyEvent) { if let Err(e) = self.event_sender.send(event) { - log::error!(target: "app", "发送代理事件失败: {e}"); + logging!(error, Type::Network, "发送代理事件失败: {e}"); } } @@ -143,7 +144,7 @@ impl EventDrivenProxyManager { event_rx: mpsc::UnboundedReceiver, query_rx: mpsc::UnboundedReceiver, ) { - log::info!(target: "app", "事件驱动代理管理器启动"); + logging!(info, Type::Network, "事件驱动代理管理器启动"); // 将 mpsc 接收器包装成 Stream,避免每次循环创建 future let mut event_stream = UnboundedReceiverStream::new(event_rx); @@ -158,7 +159,7 @@ impl EventDrivenProxyManager { loop { tokio::select! { Some(event) = event_stream.next() => { - log::debug!(target: "app", "处理代理事件: {event:?}"); + logging!(debug, Type::Network, "处理代理事件: {event:?}"); let event_clone = event.clone(); // 保存一份副本用于后续检查 Self::handle_event(&state, event).await; @@ -179,13 +180,13 @@ impl EventDrivenProxyManager { // 定时检查代理设置 let config = Self::get_proxy_config().await; if config.guard_enabled && config.sys_enabled { - log::debug!(target: "app", "定时检查代理设置"); + logging!(debug, Type::Network, "定时检查代理设置"); Self::check_and_restore_proxy(&state).await; } } else => { // 两个通道都关闭时退出 - log::info!(target: "app", "事件或查询通道关闭,代理管理器停止"); + logging!(info, Type::Network, "事件或查询通道关闭,代理管理器停止"); break; } } @@ -201,7 +202,7 @@ impl EventDrivenProxyManager { Self::initialize_proxy_state(state).await; } ProxyEvent::AppStopping => { - log::info!(target: "app", "清理代理状态"); + logging!(info, Type::Network, "清理代理状态"); Self::update_state_timestamp(state, |s| { s.sys_enabled = false; s.pac_enabled = false; @@ -224,7 +225,7 @@ impl EventDrivenProxyManager { } async fn initialize_proxy_state(state: &Arc>) { - log::info!(target: "app", "初始化代理状态"); + logging!(info, Type::Network, "初始化代理状态"); let config = Self::get_proxy_config().await; let auto_proxy = Self::get_auto_proxy_with_timeout().await; @@ -239,11 +240,17 @@ impl EventDrivenProxyManager { }) .await; - log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled); + logging!( + info, + Type::Network, + "代理状态初始化完成: sys={}, pac={}", + config.sys_enabled, + config.pac_enabled + ); } async fn update_proxy_config(state: &Arc>) { - log::debug!(target: "app", "更新代理配置"); + logging!(debug, Type::Network, "更新代理配置"); let config = Self::get_proxy_config().await; @@ -260,7 +267,7 @@ impl EventDrivenProxyManager { async fn check_and_restore_proxy(state: &Arc>) { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过系统代理守卫检查"); + logging!(debug, Type::Network, "应用正在退出,跳过系统代理守卫检查"); return; } let (sys_enabled, pac_enabled) = { @@ -272,7 +279,7 @@ impl EventDrivenProxyManager { return; } - log::debug!(target: "app", "检查代理状态"); + logging!(debug, Type::Network, "检查代理状态"); if pac_enabled { Self::check_and_restore_pac_proxy(state).await; @@ -283,7 +290,7 @@ impl EventDrivenProxyManager { async fn check_and_restore_pac_proxy(state: &Arc>) { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过PAC代理恢复检查"); + logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复检查"); return; } @@ -296,9 +303,9 @@ impl EventDrivenProxyManager { .await; if !current.enable || current.url != expected.url { - log::info!(target: "app", "PAC代理设置异常,正在恢复..."); + logging!(info, Type::Network, "PAC代理设置异常,正在恢复..."); if let Err(e) = Self::restore_pac_proxy(&expected.url).await { - log::error!(target: "app", "恢复PAC代理失败: {}", e); + logging!(error, Type::Network, "恢复PAC代理失败: {}", e); } sleep(Duration::from_millis(500)).await; @@ -314,7 +321,7 @@ impl EventDrivenProxyManager { async fn check_and_restore_sys_proxy(state: &Arc>) { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过系统代理恢复检查"); + logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复检查"); return; } @@ -327,9 +334,9 @@ impl EventDrivenProxyManager { .await; if !current.enable || current.host != expected.host || current.port != expected.port { - log::info!(target: "app", "系统代理设置异常,正在恢复..."); + logging!(info, Type::Network, "系统代理设置异常,正在恢复..."); if let Err(e) = Self::restore_sys_proxy(&expected).await { - log::error!(target: "app", "恢复系统代理失败: {}", e); + logging!(error, Type::Network, "恢复系统代理失败: {}", e); } sleep(Duration::from_millis(500)).await; @@ -457,7 +464,7 @@ impl EventDrivenProxyManager { #[cfg(target_os = "windows")] async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过PAC代理恢复"); + logging!(debug, Type::Network, "应用正在退出,跳过PAC代理恢复"); return Ok(()); } Self::execute_sysproxy_command(&["pac", expected_url]).await @@ -481,7 +488,7 @@ impl EventDrivenProxyManager { #[cfg(target_os = "windows")] async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过系统代理恢复"); + logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复"); return Ok(()); } let address = format!("{}:{}", expected.host, expected.port); @@ -502,8 +509,9 @@ impl EventDrivenProxyManager { #[cfg(target_os = "windows")] async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> { if handle::Handle::global().is_exiting() { - log::debug!( - target: "app", + logging!( + debug, + Type::Network, "应用正在退出,取消调用 sysproxy.exe,参数: {:?}", args ); @@ -518,14 +526,14 @@ impl EventDrivenProxyManager { let binary_path = match dirs::service_path() { Ok(path) => path, Err(e) => { - log::error!(target: "app", "获取服务路径失败: {e}"); + logging!(error, Type::Network, "获取服务路径失败: {e}"); return Err(e); } }; let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); if !sysproxy_exe.exists() { - log::error!(target: "app", "sysproxy.exe 不存在"); + logging!(error, Type::Network, "sysproxy.exe 不存在"); } anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist"); diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 1a397438..d9429c76 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -8,7 +8,6 @@ use anyhow::{Result, bail}; use parking_lot::Mutex; use smartstring::alias::String; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; -use tauri::{AppHandle, Manager}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; /// Enum representing all available hotkey functions @@ -105,66 +104,53 @@ impl Hotkey { } /// Execute the function associated with a hotkey function enum - fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) { - let app_handle = app_handle.clone(); + fn execute_function(function: HotkeyFunction) { match function { HotkeyFunction::OpenOrCloseDashboard => { AsyncHandler::spawn(async move || { crate::feat::open_or_close_dashboard().await; - notify_event(app_handle, NotificationEvent::DashboardToggled).await; + notify_event(NotificationEvent::DashboardToggled).await; }); } HotkeyFunction::ClashModeRule => { AsyncHandler::spawn(async move || { feat::change_clash_mode("rule".into()).await; - notify_event( - app_handle, - NotificationEvent::ClashModeChanged { mode: "Rule" }, - ) - .await; + notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await; }); } HotkeyFunction::ClashModeGlobal => { AsyncHandler::spawn(async move || { feat::change_clash_mode("global".into()).await; - notify_event( - app_handle, - NotificationEvent::ClashModeChanged { mode: "Global" }, - ) - .await; + notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await; }); } HotkeyFunction::ClashModeDirect => { AsyncHandler::spawn(async move || { feat::change_clash_mode("direct".into()).await; - notify_event( - app_handle, - NotificationEvent::ClashModeChanged { mode: "Direct" }, - ) - .await; + notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await; }); } HotkeyFunction::ToggleSystemProxy => { AsyncHandler::spawn(async move || { feat::toggle_system_proxy().await; - notify_event(app_handle, NotificationEvent::SystemProxyToggled).await; + notify_event(NotificationEvent::SystemProxyToggled).await; }); } HotkeyFunction::ToggleTunMode => { AsyncHandler::spawn(async move || { feat::toggle_tun_mode(None).await; - notify_event(app_handle, NotificationEvent::TunModeToggled).await; + notify_event(NotificationEvent::TunModeToggled).await; }); } HotkeyFunction::EntryLightweightMode => { AsyncHandler::spawn(async move || { entry_lightweight_mode().await; - notify_event(app_handle, NotificationEvent::LightweightModeEntered).await; + notify_event(NotificationEvent::LightweightModeEntered).await; }); } HotkeyFunction::Quit => { AsyncHandler::spawn(async move || { - notify_event(app_handle, NotificationEvent::AppQuit).await; + notify_event(NotificationEvent::AppQuit).await; feat::quit().await; }); } @@ -172,7 +158,7 @@ impl Hotkey { HotkeyFunction::Hide => { AsyncHandler::spawn(async move || { feat::hide().await; - notify_event(app_handle, NotificationEvent::AppHidden).await; + notify_event(NotificationEvent::AppHidden).await; }); } } @@ -224,14 +210,12 @@ impl Hotkey { let is_quit = matches!(function, HotkeyFunction::Quit); - manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| { + manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| { let hotkey_event_owned = *hotkey_event; let event_owned = event; let function_owned = function; let is_quit_owned = is_quit; - let app_handle_cloned = app_handle.clone(); - AsyncHandler::spawn(move || async move { if event_owned.state == ShortcutState::Pressed { logging!( @@ -242,11 +226,11 @@ impl Hotkey { ); if hotkey_event_owned.key == Code::KeyQ && is_quit_owned { - if let Some(window) = app_handle_cloned.get_webview_window("main") + if let Some(window) = handle::Handle::get_window() && window.is_focused().unwrap_or(false) { logging!(debug, Type::Hotkey, "Executing quit function"); - Self::execute_function(function_owned, &app_handle_cloned); + Self::execute_function(function_owned); } } else { logging!(debug, Type::Hotkey, "Executing function directly"); @@ -258,14 +242,14 @@ impl Hotkey { .unwrap_or(true); if is_enable_global_hotkey { - Self::execute_function(function_owned, &app_handle_cloned); + Self::execute_function(function_owned); } else { use crate::utils::window_manager::WindowManager; let is_visible = WindowManager::is_main_window_visible(); let is_focused = WindowManager::is_main_window_focused(); if is_focused && is_visible { - Self::execute_function(function_owned, &app_handle_cloned); + Self::execute_function(function_owned); } } } diff --git a/src-tauri/src/core/logger.rs b/src-tauri/src/core/logger.rs index ff3d8a0e..8fd38c3d 100644 --- a/src-tauri/src/core/logger.rs +++ b/src-tauri/src/core/logger.rs @@ -1,38 +1,6 @@ -use std::{collections::VecDeque, sync::Arc}; +use std::sync::Arc; -use compact_str::CompactString; -use once_cell::sync::OnceCell; -use parking_lot::{RwLock, RwLockReadGuard}; +use clash_verge_logger::AsyncLogger; +use once_cell::sync::Lazy; -const LOGS_QUEUE_LEN: usize = 100; - -pub struct ClashLogger { - logs: Arc>>, -} - -impl ClashLogger { - pub fn global() -> &'static ClashLogger { - static LOGGER: OnceCell = OnceCell::new(); - - LOGGER.get_or_init(|| ClashLogger { - logs: Arc::new(RwLock::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))), - }) - } - - pub fn get_logs(&self) -> RwLockReadGuard<'_, VecDeque> { - self.logs.read() - } - - pub fn append_log(&self, text: CompactString) { - let mut logs = self.logs.write(); - if logs.len() > LOGS_QUEUE_LEN { - logs.pop_front(); - } - logs.push_back(text); - } - - pub fn clear_logs(&self) { - let mut logs = self.logs.write(); - logs.clear(); - } -} +pub static CLASH_LOGGER: Lazy> = Lazy::new(|| Arc::new(AsyncLogger::new())); diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index 263ddb4b..70b4bc6f 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -19,11 +19,11 @@ impl CoreManager { let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); let clash_config = Config::clash().await.latest_ref().0.clone(); - *Config::runtime().await.draft_mut() = Box::new(IRuntime { + **Config::runtime().await.draft_mut() = IRuntime { config: Some(clash_config.clone()), exists_keys: vec![], chain_logs: Default::default(), - }); + }; help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?; handle::Handle::notice_message(error_key, error_msg); diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 4dfbae88..8f0c6a88 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -1,7 +1,8 @@ use super::{CoreManager, RunningMode}; +use crate::config::{Config, ConfigType, IVerge}; use crate::{ core::{ - logger::ClashLogger, + logger::CLASH_LOGGER, service::{SERVICE_MANAGER, ServiceStatus}, }, logging, @@ -14,16 +15,16 @@ impl CoreManager { pub async fn start_core(&self) -> Result<()> { self.prepare_startup().await?; - match self.get_running_mode() { + match *self.get_running_mode() { RunningMode::Service => self.start_core_by_service().await, RunningMode::NotRunning | RunningMode::Sidecar => self.start_core_by_sidecar().await, } } pub async fn stop_core(&self) -> Result<()> { - ClashLogger::global().clear_logs(); + CLASH_LOGGER.clear_logs().await; - match self.get_running_mode() { + match *self.get_running_mode() { RunningMode::Service => self.stop_core_by_service().await, RunningMode::Sidecar => self.stop_core_by_sidecar(), RunningMode::NotRunning => Ok(()), @@ -41,18 +42,12 @@ impl CoreManager { self.start_core().await } - pub async fn change_core(&self, clash_core: Option) -> Result<(), String> { - use crate::config::{Config, ConfigType, IVerge}; - - let core = clash_core - .as_ref() - .ok_or_else(|| "Clash core cannot be None".to_string())?; - - if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) { - return Err(format!("Invalid clash core: {}", core).into()); + pub async fn change_core(&self, clash_core: &String) -> Result<(), String> { + if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) { + return Err(format!("Invalid clash core: {}", clash_core).into()); } - Config::verge().await.draft_mut().clash_core = clash_core; + Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into(); Config::verge().await.apply(); let verge_data = Config::verge().await.latest_ref().clone(); diff --git a/src-tauri/src/core/manager/mod.rs b/src-tauri/src/core/manager/mod.rs index 88d5cdb7..05220f9e 100644 --- a/src-tauri/src/core/manager/mod.rs +++ b/src-tauri/src/core/manager/mod.rs @@ -1,6 +1,5 @@ mod config; mod lifecycle; -mod process; mod state; use anyhow::Result; @@ -11,7 +10,7 @@ use tokio::sync::Semaphore; use crate::process::CommandChildGuard; use crate::singleton_lazy; -#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq)] +#[derive(Debug, serde::Serialize, PartialEq, Eq)] pub enum RunningMode { Service, Sidecar, @@ -37,14 +36,14 @@ pub struct CoreManager { #[derive(Debug)] struct State { - running_mode: RunningMode, + running_mode: Arc, child_sidecar: Option, } impl Default for State { fn default() -> Self { Self { - running_mode: RunningMode::NotRunning, + running_mode: Arc::new(RunningMode::NotRunning), child_sidecar: None, } } @@ -61,16 +60,19 @@ impl Default for CoreManager { } impl CoreManager { - pub fn get_running_mode(&self) -> RunningMode { - self.state.lock().running_mode + pub fn get_running_mode(&self) -> Arc { + Arc::clone(&self.state.lock().running_mode) } pub fn set_running_mode(&self, mode: RunningMode) { - self.state.lock().running_mode = mode; + self.state.lock().running_mode = Arc::new(mode); + } + + pub fn set_running_child_sidecar(&self, child: CommandChildGuard) { + self.state.lock().child_sidecar = Some(child); } pub async fn init(&self) -> Result<()> { - self.cleanup_orphaned_processes().await?; self.start_core().await?; Ok(()) } diff --git a/src-tauri/src/core/manager/process.rs b/src-tauri/src/core/manager/process.rs deleted file mode 100644 index ccb32961..00000000 --- a/src-tauri/src/core/manager/process.rs +++ /dev/null @@ -1,244 +0,0 @@ -use super::CoreManager; -#[cfg(windows)] -use crate::process::AsyncHandler; -use crate::{ - constants::{process, timing}, - logging, - utils::logging::Type, -}; -use anyhow::Result; -#[cfg(windows)] -use anyhow::anyhow; - -impl CoreManager { - pub async fn cleanup_orphaned_processes(&self) -> Result<()> { - logging!(info, Type::Core, "Cleaning orphaned mihomo processes"); - - let current_pid = self - .state - .lock() - .child_sidecar - .as_ref() - .and_then(|c| c.pid()); - let target_processes = process::process_names(); - - let process_futures = target_processes.iter().map(|&name| { - let process_name = process::with_extension(name); - self.find_processes_by_name(process_name, name) - }); - - let process_results = futures::future::join_all(process_futures).await; - - let pids_to_kill: Vec<_> = process_results - .into_iter() - .filter_map(Result::ok) - .flat_map(|(pids, name)| { - pids.into_iter() - .filter(move |&pid| Some(pid) != current_pid) - .map(move |pid| (pid, name.clone())) - }) - .collect(); - - if pids_to_kill.is_empty() { - return Ok(()); - } - - let kill_futures = pids_to_kill - .iter() - .map(|(pid, name)| self.kill_process_verified(*pid, name.clone())); - - let killed_count = futures::future::join_all(kill_futures) - .await - .into_iter() - .filter(|&success| success) - .count(); - - if killed_count > 0 { - logging!( - info, - Type::Core, - "Cleaned {} orphaned processes", - killed_count - ); - } - - Ok(()) - } - - async fn find_processes_by_name( - &self, - process_name: String, - _target: &str, - ) -> Result<(Vec, String)> { - #[cfg(windows)] - { - use std::mem; - use winapi::um::{ - handleapi::CloseHandle, - tlhelp32::{ - CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, - TH32CS_SNAPPROCESS, - }, - }; - - let process_name_clone = process_name.clone(); - let pids = AsyncHandler::spawn_blocking(move || -> Result> { - let mut pids = Vec::new(); - - unsafe { - let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE { - return Err(anyhow!("Failed to create process snapshot")); - } - - let mut pe32: PROCESSENTRY32W = mem::zeroed(); - pe32.dwSize = mem::size_of::() as u32; - - if Process32FirstW(snapshot, &mut pe32) != 0 { - loop { - let end_pos = pe32 - .szExeFile - .iter() - .position(|&x| x == 0) - .unwrap_or(pe32.szExeFile.len()); - - let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]); - if exe_file.eq_ignore_ascii_case(&process_name_clone) { - pids.push(pe32.th32ProcessID); - } - - if Process32NextW(snapshot, &mut pe32) == 0 { - break; - } - } - } - - CloseHandle(snapshot); - } - - Ok(pids) - }) - .await??; - - Ok((pids, process_name)) - } - - #[cfg(not(windows))] - { - let cmd = if cfg!(target_os = "macos") { - "pgrep" - } else { - "pidof" - }; - let output = tokio::process::Command::new(cmd) - .arg(&process_name) - .output() - .await?; - - if !output.status.success() { - return Ok((Vec::new(), process_name)); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let pids: Vec = stdout - .split_whitespace() - .filter_map(|s| s.parse().ok()) - .collect(); - - Ok((pids, process_name)) - } - } - - async fn kill_process_verified(&self, pid: u32, process_name: String) -> bool { - #[cfg(windows)] - let success = { - use winapi::um::{ - handleapi::CloseHandle, - processthreadsapi::{OpenProcess, TerminateProcess}, - winnt::{HANDLE, PROCESS_TERMINATE}, - }; - - AsyncHandler::spawn_blocking(move || unsafe { - let handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid); - if handle.is_null() { - return false; - } - let result = TerminateProcess(handle, 1) != 0; - CloseHandle(handle); - result - }) - .await - .unwrap_or(false) - }; - - #[cfg(not(windows))] - let success = tokio::process::Command::new("kill") - .args(["-9", &pid.to_string()]) - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false); - - if !success { - return false; - } - - tokio::time::sleep(timing::PROCESS_VERIFY_DELAY).await; - - if self.is_process_running(pid).await.unwrap_or(false) { - logging!( - warn, - Type::Core, - "Process {} (PID: {}) still running after termination", - process_name, - pid - ); - false - } else { - logging!( - info, - Type::Core, - "Terminated process {} (PID: {})", - process_name, - pid - ); - true - } - } - - async fn is_process_running(&self, pid: u32) -> Result { - #[cfg(windows)] - { - use winapi::{ - shared::minwindef::DWORD, - um::{ - handleapi::CloseHandle, - processthreadsapi::{GetExitCodeProcess, OpenProcess}, - winnt::{HANDLE, PROCESS_QUERY_INFORMATION}, - }, - }; - - AsyncHandler::spawn_blocking(move || unsafe { - let handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); - if handle.is_null() { - return Ok(false); - } - let mut exit_code: DWORD = 0; - let result = GetExitCodeProcess(handle, &mut exit_code); - CloseHandle(handle); - Ok(result != 0 && exit_code == 259) - }) - .await? - } - - #[cfg(not(windows))] - { - let output = tokio::process::Command::new("ps") - .args(["-p", &pid.to_string()]) - .output() - .await?; - - Ok(output.status.success() && !output.stdout.is_empty()) - } - } -} diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index ff01a8c1..d38a148c 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -2,7 +2,7 @@ use super::{CoreManager, RunningMode}; use crate::{ AsyncHandler, config::Config, - core::{handle, logger::ClashLogger, service}, + core::{handle, logger::CLASH_LOGGER, service}, logging, process::CommandChildGuard, utils::{ @@ -15,15 +15,15 @@ use anyhow::Result; use compact_str::CompactString; use flexi_logger::DeferredNow; use log::Level; -use std::collections::VecDeque; +use scopeguard::defer; use tauri_plugin_shell::ShellExt; impl CoreManager { - pub async fn get_clash_logs(&self) -> Result> { - match self.get_running_mode() { + pub async fn get_clash_logs(&self) -> Result> { + match *self.get_running_mode() { RunningMode::Service => service::get_clash_logs_by_service().await, - RunningMode::Sidecar => Ok(ClashLogger::global().get_logs().clone()), - RunningMode::NotRunning => Ok(VecDeque::new()), + RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().await), + RunningMode::NotRunning => Ok(Vec::new()), } } @@ -49,11 +49,8 @@ impl CoreManager { let pid = child.pid(); logging!(trace, Type::Core, "Sidecar started with PID: {}", pid); - { - let mut state = self.state.lock(); - state.child_sidecar = Some(CommandChildGuard::new(child)); - state.running_mode = RunningMode::Sidecar; - } + self.set_running_child_sidecar(CommandChildGuard::new(child)); + self.set_running_mode(RunningMode::Sidecar); let shared_writer: SharedWriter = std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?)); @@ -67,7 +64,7 @@ impl CoreManager { let message = CompactString::from(String::from_utf8_lossy(&line).as_ref()); let w = shared_writer.lock().await; write_sidecar_log(w, &mut now, Level::Error, &message); - ClashLogger::global().append_log(message); + CLASH_LOGGER.append_log(message).await; } tauri_plugin_shell::process::CommandEvent::Terminated(term) => { let mut now = DeferredNow::default(); @@ -80,7 +77,7 @@ impl CoreManager { }; let w = shared_writer.lock().await; write_sidecar_log(w, &mut now, Level::Info, &message); - ClashLogger::global().clear_logs(); + CLASH_LOGGER.clear_logs().await; break; } _ => {} @@ -93,14 +90,15 @@ impl CoreManager { pub(super) fn stop_core_by_sidecar(&self) -> Result<()> { logging!(info, Type::Core, "Stopping sidecar"); - + defer! { + self.set_running_mode(RunningMode::NotRunning); + } let mut state = self.state.lock(); if let Some(child) = state.child_sidecar.take() { let pid = child.pid(); drop(child); logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid); } - state.running_mode = RunningMode::NotRunning; Ok(()) } @@ -114,8 +112,10 @@ impl CoreManager { pub(super) async fn stop_core_by_service(&self) -> Result<()> { logging!(info, Type::Core, "Stopping service"); + defer! { + self.set_running_mode(RunningMode::NotRunning); + } service::stop_core_by_service().await?; - self.set_running_mode(RunningMode::NotRunning); Ok(()) } } diff --git a/src-tauri/src/core/notification.rs b/src-tauri/src/core/notification.rs index 08102fc0..071bcedb 100644 --- a/src-tauri/src/core/notification.rs +++ b/src-tauri/src/core/notification.rs @@ -96,10 +96,17 @@ impl NotificationSystem { let handle = Handle::global(); while !handle.is_exiting() { - match rx.recv_timeout(std::time::Duration::from_millis(100)) { + match rx.recv() { Ok(event) => Self::process_event(handle, event), - Err(mpsc::RecvTimeoutError::Disconnected) => break, - Err(mpsc::RecvTimeoutError::Timeout) => break, + Err(e) => { + logging!( + error, + Type::System, + "receive event error, stop notification worker: {}", + e + ); + break; + } } } } diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 839855ea..04ded18a 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -1,5 +1,6 @@ use crate::{ config::Config, + core::tray, logging, logging_error, utils::{dirs, init::service_writer_config, logging::Type}, }; @@ -8,7 +9,6 @@ use clash_verge_service_ipc::CoreConfig; use compact_str::CompactString; use once_cell::sync::Lazy; use std::{ - collections::VecDeque, env::current_exe, path::{Path, PathBuf}, process::Command as StdCommand, @@ -394,7 +394,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { start_with_existing_service(config_file).await } -pub(super) async fn get_clash_logs_by_service() -> Result> { +pub(super) async fn get_clash_logs_by_service() -> Result> { logging!(info, Type::Service, "正在获取服务模式下的 Clash 日志"); let response = clash_verge_service_ipc::get_clash_logs() @@ -532,6 +532,7 @@ impl ServiceManager { return Err(anyhow::anyhow!("服务不可用: {}", reason)); } } + let _ = tray::Tray::global().update_tray_display().await; Ok(()) } } diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 77e561f1..c24d8be9 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -15,6 +15,7 @@ use sysproxy::{Autoproxy, Sysproxy}; use tauri_plugin_autostart::ManagerExt; pub struct Sysopt { + initialed: AtomicBool, update_sysproxy: AtomicBool, reset_sysproxy: AtomicBool, } @@ -84,6 +85,7 @@ async fn execute_sysproxy_command(args: Vec) -> Result<()> impl Default for Sysopt { fn default() -> Self { Sysopt { + initialed: AtomicBool::new(false), update_sysproxy: AtomicBool::new(false), reset_sysproxy: AtomicBool::new(false), } @@ -94,17 +96,22 @@ impl Default for Sysopt { singleton_lazy!(Sysopt, SYSOPT, Sysopt::default); impl Sysopt { + pub fn is_initialed(&self) -> bool { + self.initialed.load(Ordering::SeqCst) + } + pub fn init_guard_sysproxy(&self) -> Result<()> { // 使用事件驱动代理管理器 let proxy_manager = EventDrivenProxyManager::global(); proxy_manager.notify_app_started(); - log::info!(target: "app", "已启用事件驱动代理守卫"); + logging!(info, Type::Core, "已启用事件驱动代理守卫"); Ok(()) } /// init the sysproxy pub async fn update_sysproxy(&self) -> Result<()> { + self.initialed.store(true, Ordering::SeqCst); if self .update_sysproxy .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) @@ -224,14 +231,22 @@ impl Sysopt { let mut sysproxy: Sysproxy = match Sysproxy::get_system_proxy() { Ok(sp) => sp, Err(e) => { - log::warn!(target: "app", "重置代理时获取系统代理配置失败: {e}, 使用默认配置"); + logging!( + warn, + Type::Core, + "Warning: 重置代理时获取系统代理配置失败: {e}, 使用默认配置" + ); Sysproxy::default() } }; let mut autoproxy = match Autoproxy::get_auto_proxy() { Ok(ap) => ap, Err(e) => { - log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置"); + logging!( + warn, + Type::Core, + "Warning: 重置代理时获取自动代理配置失败: {e}, 使用默认配置" + ); Autoproxy::default() } }; @@ -265,14 +280,14 @@ impl Sysopt { { if is_enable { if let Err(e) = startup_shortcut::create_shortcut().await { - log::error!(target: "app", "创建启动快捷方式失败: {e}"); + logging!(error, Type::Setup, "创建启动快捷方式失败: {e}"); // 如果快捷方式创建失败,回退到原来的方法 self.try_original_autostart_method(is_enable); } else { return Ok(()); } } else if let Err(e) = startup_shortcut::remove_shortcut().await { - log::error!(target: "app", "删除启动快捷方式失败: {e}"); + logging!(error, Type::Setup, "删除启动快捷方式失败: {e}"); self.try_original_autostart_method(is_enable); } else { return Ok(()); @@ -307,11 +322,11 @@ impl Sysopt { { match startup_shortcut::is_shortcut_enabled() { Ok(enabled) => { - log::info!(target: "app", "快捷方式自启动状态: {enabled}"); + logging!(info, Type::System, "快捷方式自启动状态: {enabled}"); return Ok(enabled); } Err(e) => { - log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}"); + logging!(error, Type::System, "检查快捷方式失败,尝试原来的方法: {e}"); } } } @@ -322,11 +337,11 @@ impl Sysopt { match autostart_manager.is_enabled() { Ok(status) => { - log::info!(target: "app", "Auto launch status: {status}"); + logging!(info, Type::System, "Auto launch status: {status}"); Ok(status) } Err(e) => { - log::error!(target: "app", "Failed to get auto launch status: {e}"); + logging!(error, Type::System, "Failed to get auto launch status: {e}"); Err(anyhow::anyhow!("Failed to get auto launch status: {}", e)) } } diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 29eba1fd..e8506df1 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -1,4 +1,7 @@ -use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type}; +use crate::{ + config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton, + utils::logging::Type, +}; use anyhow::{Context, Result}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; use parking_lot::RwLock; @@ -10,7 +13,9 @@ use std::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, }, + time::Duration, }; +use tokio::time::{sleep, timeout}; type TaskID = u64; @@ -99,6 +104,12 @@ impl Timer { items .iter() .filter_map(|item| { + let allow_auto_update = + item.option.as_ref()?.allow_auto_update.unwrap_or_default(); + if !allow_auto_update { + return None; + } + let interval = item.option.as_ref()?.update_interval? as i64; let updated = item.updated? as i64; let uid = item.uid.as_ref()?; @@ -149,7 +160,7 @@ impl Timer { .set_maximum_parallel_runnable_num(1) .set_frequency_count_down_by_seconds(3, 3) .spawn_async_routine(|| async move { - logging!(info, Type::Timer, "Updating tray menu"); + logging!(debug, Type::Timer, "Updating tray menu"); crate::core::tray::Tray::global() .update_tray_display() .await @@ -384,7 +395,8 @@ impl Timer { .spawn_async_routine(move || { let uid = uid.clone(); Box::pin(async move { - Self::async_task(uid).await; + Self::wait_until_sysopt(Duration::from_millis(1000)).await; + Self::async_task(&uid).await; }) as Pin + Send>> }) .context("failed to create timer task")?; @@ -413,13 +425,15 @@ impl Timer { }; // Get the profile updated timestamp - now safe to await - let config_profiles = Config::profiles().await; - let profiles = config_profiles.data_ref().clone(); - let items = match profiles.get_items() { - Some(i) => i, - None => { - logging!(warn, Type::Timer, "获取配置列表失败"); - return None; + let items = { + let profiles = Config::profiles().await; + let profiles_guard = profiles.latest_ref(); + match profiles_guard.get_items() { + Some(i) => i.clone(), + None => { + logging!(warn, Type::Timer, "获取配置列表失败"); + return None; + } } }; @@ -468,14 +482,14 @@ impl Timer { } /// Async task with better error handling and logging - async fn async_task(uid: String) { + async fn async_task(uid: &String) { let task_start = std::time::Instant::now(); logging!(info, Type::Timer, "Running timer task for profile: {}", uid); match tokio::time::timeout(std::time::Duration::from_secs(40), async { - Self::emit_update_event(&uid, true); + Self::emit_update_event(uid, true); - let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid); + let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(uid); logging!( info, Type::Timer, @@ -484,7 +498,7 @@ impl Timer { is_current ); - feat::update_profile(uid.clone(), None, Some(is_current)).await + feat::update_profile(uid, None, is_current, false).await }) .await { @@ -509,7 +523,17 @@ impl Timer { } // Emit completed event - Self::emit_update_event(&uid, false); + Self::emit_update_event(uid, false); + } + + async fn wait_until_sysopt(max_wait: Duration) { + let _ = timeout(max_wait, async { + while !Sysopt::global().is_initialed() { + logging!(warn, Type::Timer, "Waiting for Sysopt to be initialized..."); + sleep(Duration::from_millis(30)).await; + } + }) + .await; } } diff --git a/src-tauri/src/core/tray/menu_def.rs b/src-tauri/src/core/tray/menu_def.rs new file mode 100644 index 00000000..10fe3835 --- /dev/null +++ b/src-tauri/src/core/tray/menu_def.rs @@ -0,0 +1,49 @@ +use crate::utils::i18n::t; +use std::sync::Arc; + +macro_rules! define_menu { + ($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => { + #[derive(Debug)] + pub struct MenuTexts { + $(pub $field: Arc,)+ + } + + pub struct MenuIds; + + impl MenuTexts { + pub async fn new() -> Self { + let ($($field,)+) = futures::join!($(t($text),)+); + Self { $($field,)+ } + } + } + + impl MenuIds { + $(pub const $const_name: &'static str = $id;)+ + } + }; +} + +define_menu! { + dashboard => DASHBOARD, "tray_dashboard", "Dashboard", + rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode", + global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode", + direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode", + profiles => PROFILES, "tray_profiles", "Profiles", + proxies => PROXIES, "tray_proxies", "Proxies", + system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy", + tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode", + close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections", + lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode", + copy_env => COPY_ENV, "tray_copy_env", "Copy Env", + conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir", + core_dir => CORE_DIR, "tray_core_dir", "Core Dir", + logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir", + open_dir => OPEN_DIR, "tray_open_dir", "Open Dir", + app_log => APP_LOG, "tray_app_log", "Open App Log", + core_log => CORE_LOG, "tray_core_log", "Open Core Log", + restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core", + restart_app => RESTART_APP, "tray_restart_app", "Restart App", + verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version", + more => MORE, "tray_more", "More", + exit => EXIT, "tray_exit", "Exit", +} diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 2295c811..ef300bbe 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -1,8 +1,12 @@ use once_cell::sync::OnceCell; use tauri::Emitter; use tauri::tray::TrayIconBuilder; +use tauri_plugin_mihomo::models::Proxies; +use tokio::fs; #[cfg(target_os = "macos")] pub mod speed_rate; +use crate::config::PrfSelected; +use crate::core::service; use crate::module::lightweight; use crate::process::AsyncHandler; use crate::utils::window_manager::WindowManager; @@ -21,8 +25,8 @@ use futures::future::join_all; use parking_lot::Mutex; use smartstring::alias::String; use std::collections::HashMap; +use std::sync::Arc; use std::{ - fs, sync::atomic::{AtomicBool, Ordering}, time::{Duration, Instant}, }; @@ -31,9 +35,13 @@ use tauri::{ menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, tray::{MouseButton, MouseButtonState, TrayIconEvent}, }; +mod menu_def; +use menu_def::{MenuIds, MenuTexts}; // TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑) +type ProxyMenuItem = (Option>, Vec>>); + #[derive(Clone)] struct TrayState {} @@ -54,8 +62,12 @@ fn should_handle_tray_click() -> bool { *last_click = now; true } else { - log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms", - now.duration_since(*last_click).as_millis()); + logging!( + debug, + Type::Tray, + "托盘点击被防抖机制忽略,距离上次点击 {}ms", + now.duration_since(*last_click).as_millis() + ); false } } @@ -78,7 +90,7 @@ impl TrayState { let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); if is_common_tray_icon && let Ok(Some(common_icon_path)) = find_target_icons("common") - && let Ok(icon_data) = fs::read(common_icon_path) + && let Ok(icon_data) = fs::read(common_icon_path).await { return (true, icon_data); } @@ -115,7 +127,7 @@ impl TrayState { let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); if is_sysproxy_tray_icon && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") - && let Ok(icon_data) = fs::read(sysproxy_icon_path) + && let Ok(icon_data) = fs::read(sysproxy_icon_path).await { return (true, icon_data); } @@ -152,7 +164,7 @@ impl TrayState { let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); if is_tun_tray_icon && let Ok(Some(tun_icon_path)) = find_target_icons("tun") - && let Ok(icon_data) = fs::read(tun_icon_path) + && let Ok(icon_data) = fs::read(tun_icon_path).await { return (true, icon_data); } @@ -199,7 +211,7 @@ singleton_lazy!(Tray, TRAY, Tray::default); impl Tray { pub async fn init(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘初始化"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化"); return Ok(()); } @@ -207,11 +219,15 @@ impl Tray { match self.create_tray_from_handle(app_handle).await { Ok(_) => { - log::info!(target: "app", "System tray created successfully"); + logging!(info, Type::Tray, "System tray created successfully"); } Err(e) => { // Don't return error, let application continue running without tray - log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e); + logging!( + warn, + Type::Tray, + "System tray creation failed: {e}, Application will continue running without tray icon", + ); } } // TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白 @@ -222,7 +238,7 @@ impl Tray { /// 更新托盘点击行为 pub async fn update_click_behavior(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘点击行为更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘点击行为更新"); return Ok(()); } @@ -242,7 +258,7 @@ impl Tray { /// 更新托盘菜单 pub async fn update_menu(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘菜单更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新"); return Ok(()); } // 调整最小更新间隔,确保状态及时刷新 @@ -290,6 +306,8 @@ impl Tray { let verge = Config::verge().await.latest_ref().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); + let tun_mode_available = cmd::system::is_admin().unwrap_or_default() + || service::is_service_available().await.is_ok(); let mode = { Config::clash() .await @@ -315,16 +333,21 @@ impl Tray { Some(mode.as_str()), *system_proxy, *tun_mode, + tun_mode_available, profile_uid_and_name, is_lightweight_mode, ) .await?, )); - log::debug!(target: "app", "托盘菜单更新成功"); + logging!(debug, Type::Tray, "托盘菜单更新成功"); Ok(()) } None => { - log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在"); + logging!( + warn, + Type::Tray, + "Failed to update tray menu: tray not found" + ); Ok(()) } } @@ -334,7 +357,7 @@ impl Tray { #[cfg(target_os = "macos")] pub async fn update_icon(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘图标更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); } @@ -343,7 +366,11 @@ impl Tray { let tray = match app_handle.tray_by_id("main") { Some(tray) => tray, None => { - log::warn!(target: "app", "更新托盘图标失败: 托盘不存在"); + logging!( + warn, + Type::Tray, + "Failed to update tray icon: tray not found" + ); return Ok(()); } }; @@ -373,7 +400,7 @@ impl Tray { #[cfg(not(target_os = "macos"))] pub async fn update_icon(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘图标更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); } @@ -382,7 +409,11 @@ impl Tray { let tray = match app_handle.tray_by_id("main") { Some(tray) => tray, None => { - log::warn!(target: "app", "更新托盘图标失败: 托盘不存在"); + logging!( + warn, + Type::Tray, + "Failed to update tray icon: tray not found" + ); return Ok(()); } }; @@ -405,7 +436,7 @@ impl Tray { /// 更新托盘显示状态的函数 pub async fn update_tray_display(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘显示状态更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新"); return Ok(()); } @@ -423,7 +454,7 @@ impl Tray { /// 更新托盘提示 pub async fn update_tooltip(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘提示更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘提示更新"); return Ok(()); } @@ -479,7 +510,11 @@ impl Tray { if let Some(tray) = app_handle.tray_by_id("main") { let _ = tray.set_tooltip(Some(&tooltip)); } else { - log::warn!(target: "app", "更新托盘提示失败: 托盘不存在"); + logging!( + warn, + Type::Tray, + "Failed to update tray tooltip: tray not found" + ); } Ok(()) @@ -487,7 +522,7 @@ impl Tray { pub async fn update_part(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘局部更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新"); return Ok(()); } // self.update_menu().await?; @@ -500,11 +535,11 @@ impl Tray { pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘创建"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建"); return Ok(()); } - log::info!(target: "app", "正在从AppHandle创建系统托盘"); + logging!(info, Type::Tray, "正在从AppHandle创建系统托盘"); // 获取图标 let icon_bytes = TrayState::get_common_tray_icon().await.1; @@ -550,7 +585,7 @@ impl Tray { AsyncHandler::spawn(|| async move { let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); - log::debug!(target: "app", "tray event: {tray_event:?}"); + logging!(debug, Type::Tray, "tray event: {tray_event:?}"); if let TrayIconEvent::Click { button: MouseButton::Left, @@ -585,14 +620,13 @@ impl Tray { }); }); tray.on_menu_event(on_menu_event); - log::info!(target: "app", "系统托盘创建成功"); Ok(()) } // 托盘统一的状态更新函数 pub async fn update_all_states(&self) -> Result<()> { if handle::Handle::global().is_exiting() { - log::debug!(target: "app", "应用正在退出,跳过托盘状态更新"); + logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新"); return Ok(()); } @@ -606,70 +640,8 @@ impl Tray { } } -async fn create_tray_menu( - app_handle: &AppHandle, - mode: Option<&str>, - system_proxy_enabled: bool, - tun_mode_enabled: bool, - profile_uid_and_name: Vec<(String, String)>, - is_lightweight_mode: bool, -) -> Result> { - let mode = mode.unwrap_or(""); - - // 获取当前配置文件的选中代理组信息 - let current_profile_selected = { - let profiles_config = Config::profiles().await; - let profiles_ref = profiles_config.latest_ref(); - profiles_ref - .get_current() - .and_then(|uid| profiles_ref.get_item(&uid).ok()) - .and_then(|profile| profile.selected.clone()) - .unwrap_or_default() - }; - - let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await; - - let runtime_proxy_groups_order = cmd::get_runtime_config() - .await - .map_err(|e| { - logging!( - error, - Type::Cmd, - "Failed to fetch runtime proxy groups for tray menu: {e}" - ); - }) - .ok() - .flatten() - .map(|config| { - config - .get("proxy-groups") - .and_then(|groups| groups.as_sequence()) - .map(|groups| { - groups - .iter() - .filter_map(|group| group.get("name")) - .filter_map(|name| name.as_str()) - .map(|name| name.into()) - .collect::>() - }) - .unwrap_or_default() - }); - - let proxy_group_order_map = runtime_proxy_groups_order.as_ref().map(|group_names| { - group_names - .iter() - .enumerate() - .map(|(index, name)| (name.clone(), index)) - .collect::>() - }); - - let verge_settings = Config::verge().await.latest_ref().clone(); - let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false); - - let version = env!("CARGO_PKG_VERSION"); - - let hotkeys = verge_settings - .hotkeys +fn create_hotkeys(hotkeys: &Option>) -> HashMap { + hotkeys .as_ref() .map(|h| { h.iter() @@ -689,35 +661,43 @@ async fn create_tray_menu( }) .collect::>() }) - .unwrap_or_default(); + .unwrap_or_default() +} - let profile_menu_items: Vec> = { - let futures = profile_uid_and_name - .iter() - .map(|(profile_uid, profile_name)| { - let app_handle = app_handle.clone(); - let profile_uid = profile_uid.clone(); - let profile_name = profile_name.clone(); - async move { - let is_current_profile = Config::profiles() - .await - .data_mut() - .is_current_profile_index(profile_uid.clone()); - CheckMenuItem::with_id( - &app_handle, - format!("profiles_{profile_uid}"), - t(&profile_name).await, - true, - is_current_profile, - None::<&str>, - ) - } - }); - let results = join_all(futures).await; - results.into_iter().collect::, _>>()? - }; +async fn create_profile_menu_item( + app_handle: &AppHandle, + profile_uid_and_name: Vec<(String, String)>, +) -> Result>> { + let futures = profile_uid_and_name + .iter() + .map(|(profile_uid, profile_name)| { + let app_handle = app_handle.clone(); + async move { + let is_current_profile = Config::profiles() + .await + .latest_ref() + .is_current_profile_index(profile_uid); + CheckMenuItem::with_id( + &app_handle, + format!("profiles_{profile_uid}"), + t(profile_name).await, + true, + is_current_profile, + None::<&str>, + ) + } + }); + let results = join_all(futures).await; + Ok(results.into_iter().collect::, _>>()?) +} - // 代理组子菜单 +fn create_subcreate_proxy_menu_item( + app_handle: &AppHandle, + proxy_mode: &str, + current_profile_selected: &[PrfSelected], + proxy_group_order_map: Option>, + proxy_nodes_data: Result, +) -> Result>> { let proxy_submenus: Vec> = { let mut submenus: Vec<(String, usize, Submenu)> = Vec::new(); @@ -725,7 +705,7 @@ async fn create_tray_menu( if let Ok(proxy_nodes_data) = proxy_nodes_data { for (group_name, group_data) in proxy_nodes_data.proxies.iter() { // Filter groups based on mode - let should_show = match mode { + let should_show = match proxy_mode { "global" => group_name == "GLOBAL", _ => group_name != "GLOBAL", } && @@ -771,7 +751,9 @@ async fn create_tray_menu( is_selected, None::<&str>, ) - .map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e)) + .map_err(|e| { + logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e) + }) .ok() }) .collect(); @@ -781,7 +763,7 @@ async fn create_tray_menu( } // Determine if group is active - let is_group_active = match mode { + let is_group_active = match proxy_mode { "global" => group_name == "GLOBAL" && !now_proxy.is_empty(), "direct" => false, _ => { @@ -813,7 +795,12 @@ async fn create_tray_menu( let insertion_index = submenus.len(); submenus.push((group_name.into(), insertion_index, submenu)); } else { - log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name); + logging!( + warn, + Type::Tray, + "Failed to create proxy group submenu: {}", + group_name + ); } } } @@ -837,29 +824,118 @@ async fn create_tray_menu( .map(|(_, _, submenu)| submenu) .collect() }; + Ok(proxy_submenus) +} + +fn create_proxy_menu_item( + app_handle: &AppHandle, + show_proxy_groups_inline: bool, + proxy_submenus: Vec>, + proxies_text: &Arc, +) -> Result { + // 创建代理主菜单 + let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline { + ( + None, + proxy_submenus + .into_iter() + .map(|submenu| Box::new(submenu) as Box>) + .collect(), + ) + } else if !proxy_submenus.is_empty() { + let proxy_submenu_refs: Vec<&dyn IsMenuItem> = proxy_submenus + .iter() + .map(|submenu| submenu as &dyn IsMenuItem) + .collect(); + + ( + Some(Submenu::with_id_and_items( + app_handle, + MenuIds::PROXIES, + proxies_text, + true, + &proxy_submenu_refs, + )?), + Vec::new(), + ) + } else { + (None, Vec::new()) + }; + Ok((proxies_submenu, inline_proxy_items)) +} + +async fn create_tray_menu( + app_handle: &AppHandle, + mode: Option<&str>, + system_proxy_enabled: bool, + tun_mode_enabled: bool, + tun_mode_available: bool, + profile_uid_and_name: Vec<(String, String)>, + is_lightweight_mode: bool, +) -> Result> { + let current_proxy_mode = mode.unwrap_or(""); + + // 获取当前配置文件的选中代理组信息 + let current_profile_selected = { + let profiles_config = Config::profiles().await; + let profiles_ref = profiles_config.latest_ref(); + profiles_ref + .get_current() + .and_then(|uid| profiles_ref.get_item(&uid).ok()) + .and_then(|profile| profile.selected.clone()) + .unwrap_or_default() + }; + + let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await; + + let runtime_proxy_groups_order = cmd::get_runtime_config() + .await + .map_err(|e| { + logging!( + error, + Type::Cmd, + "Failed to fetch runtime proxy groups for tray menu: {e}" + ); + }) + .ok() + .flatten() + .map(|config| { + config + .get("proxy-groups") + .and_then(|groups| groups.as_sequence()) + .map(|groups| { + groups + .iter() + .filter_map(|group| group.get("name")) + .filter_map(|name| name.as_str()) + .map(|name| name.into()) + .collect::>() + }) + .unwrap_or_default() + }); + + let proxy_group_order_map: Option< + HashMap, usize>, + > = runtime_proxy_groups_order.as_ref().map(|group_names| { + group_names + .iter() + .enumerate() + .map(|(index, name)| (name.clone(), index)) + .collect::>() + }); + + let verge_settings = Config::verge().await.latest_ref().clone(); + let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false); + + let version = env!("CARGO_PKG_VERSION"); + + let hotkeys = create_hotkeys(&verge_settings.hotkeys); + + let profile_menu_items: Vec> = + create_profile_menu_item(app_handle, profile_uid_and_name).await?; // Pre-fetch all localized strings - let dashboard_text = t("Dashboard").await; - let rule_mode_text = t("Rule Mode").await; - let global_mode_text = t("Global Mode").await; - let direct_mode_text = t("Direct Mode").await; - let profiles_text = t("Profiles").await; - let proxies_text = t("Proxies").await; - let system_proxy_text = t("System Proxy").await; - let tun_mode_text = t("TUN Mode").await; - let close_all_connections_text = t("Close All Connections").await; - let lightweight_mode_text = t("LightWeight Mode").await; - let copy_env_text = t("Copy Env").await; - let conf_dir_text = t("Conf Dir").await; - let core_dir_text = t("Core Dir").await; - let logs_dir_text = t("Logs Dir").await; - let open_dir_text = t("Open Dir").await; - let restart_clash_text = t("Restart Clash Core").await; - let restart_app_text = t("Restart App").await; - let verge_version_text = t("Verge Version").await; - let more_text = t("More").await; - let exit_text = t("Exit").await; - + let texts = &MenuTexts::new().await; // Convert to references only when needed let profile_menu_items_refs: Vec<&dyn IsMenuItem> = profile_menu_items .iter() @@ -868,81 +944,66 @@ async fn create_tray_menu( let open_window = &MenuItem::with_id( app_handle, - "open_window", - dashboard_text, + MenuIds::DASHBOARD, + &texts.dashboard, true, hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()), )?; let rule_mode = &CheckMenuItem::with_id( app_handle, - "rule_mode", - rule_mode_text, + MenuIds::RULE_MODE, + &texts.rule_mode, true, - mode == "rule", + current_proxy_mode == "rule", hotkeys.get("clash_mode_rule").map(|s| s.as_str()), )?; let global_mode = &CheckMenuItem::with_id( app_handle, - "global_mode", - global_mode_text, + MenuIds::GLOBAL_MODE, + &texts.global_mode, true, - mode == "global", + current_proxy_mode == "global", hotkeys.get("clash_mode_global").map(|s| s.as_str()), )?; let direct_mode = &CheckMenuItem::with_id( app_handle, - "direct_mode", - direct_mode_text, + MenuIds::DIRECT_MODE, + &texts.direct_mode, true, - mode == "direct", + current_proxy_mode == "direct", hotkeys.get("clash_mode_direct").map(|s| s.as_str()), )?; let profiles = &Submenu::with_id_and_items( app_handle, - "profiles", - profiles_text, + MenuIds::PROFILES, + &texts.profiles, true, &profile_menu_items_refs, )?; - // 创建代理主菜单 - let (proxies_submenu, inline_proxy_items): (Option>, Vec<&dyn IsMenuItem>) = - if show_proxy_groups_inline { - ( - None, - proxy_submenus - .iter() - .map(|submenu| submenu as &dyn IsMenuItem) - .collect(), - ) - } else if !proxy_submenus.is_empty() { - let proxy_submenu_refs: Vec<&dyn IsMenuItem> = proxy_submenus - .iter() - .map(|submenu| submenu as &dyn IsMenuItem) - .collect(); + let proxy_sub_menus = create_subcreate_proxy_menu_item( + app_handle, + current_proxy_mode, + ¤t_profile_selected, + proxy_group_order_map, + proxy_nodes_data.map_err(anyhow::Error::from), + )?; - ( - Some(Submenu::with_id_and_items( - app_handle, - "proxies", - proxies_text, - true, - &proxy_submenu_refs, - )?), - Vec::new(), - ) - } else { - (None, Vec::new()) - }; + let (proxies_menu, inline_proxy_items) = create_proxy_menu_item( + app_handle, + show_proxy_groups_inline, + proxy_sub_menus, + &texts.proxies, + )?; let system_proxy = &CheckMenuItem::with_id( app_handle, - "system_proxy", - system_proxy_text, + MenuIds::SYSTEM_PROXY, + &texts.system_proxy, true, system_proxy_enabled, hotkeys.get("toggle_system_proxy").map(|s| s.as_str()), @@ -950,92 +1011,120 @@ async fn create_tray_menu( let tun_mode = &CheckMenuItem::with_id( app_handle, - "tun_mode", - tun_mode_text, - true, + MenuIds::TUN_MODE, + &texts.tun_mode, + tun_mode_available, tun_mode_enabled, hotkeys.get("toggle_tun_mode").map(|s| s.as_str()), )?; let close_all_connections = &MenuItem::with_id( app_handle, - "close_all_connections", - close_all_connections_text, + MenuIds::CLOSE_ALL_CONNECTIONS, + &texts.close_all_connections, true, None::<&str>, )?; - let lighteweight_mode = &CheckMenuItem::with_id( + let lightweight_mode = &CheckMenuItem::with_id( app_handle, - "entry_lightweight_mode", - lightweight_mode_text, + MenuIds::LIGHTWEIGHT_MODE, + &texts.lightweight_mode, true, is_lightweight_mode, hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()), )?; - let copy_env = &MenuItem::with_id(app_handle, "copy_env", copy_env_text, true, None::<&str>)?; + let copy_env = &MenuItem::with_id( + app_handle, + MenuIds::COPY_ENV, + &texts.copy_env, + true, + None::<&str>, + )?; let open_app_dir = &MenuItem::with_id( app_handle, - "open_app_dir", - conf_dir_text, + MenuIds::CONF_DIR, + &texts.conf_dir, true, None::<&str>, )?; let open_core_dir = &MenuItem::with_id( app_handle, - "open_core_dir", - core_dir_text, + MenuIds::CORE_DIR, + &texts.core_dir, true, None::<&str>, )?; let open_logs_dir = &MenuItem::with_id( app_handle, - "open_logs_dir", - logs_dir_text, + MenuIds::LOGS_DIR, + &texts.logs_dir, + true, + None::<&str>, + )?; + + let open_app_log = &MenuItem::with_id( + app_handle, + MenuIds::APP_LOG, + &texts.app_log, + true, + None::<&str>, + )?; + + let open_core_log = &MenuItem::with_id( + app_handle, + MenuIds::CORE_LOG, + &texts.core_log, true, None::<&str>, )?; let open_dir = &Submenu::with_id_and_items( app_handle, - "open_dir", - open_dir_text, + MenuIds::OPEN_DIR, + &texts.open_dir, true, - &[open_app_dir, open_core_dir, open_logs_dir], + &[ + open_app_dir, + open_core_dir, + open_logs_dir, + open_app_log, + open_core_log, + ], )?; let restart_clash = &MenuItem::with_id( app_handle, - "restart_clash", - restart_clash_text, + MenuIds::RESTART_CLASH, + &texts.restart_clash, true, None::<&str>, )?; let restart_app = &MenuItem::with_id( app_handle, - "restart_app", - restart_app_text, + MenuIds::RESTART_APP, + &texts.restart_app, true, None::<&str>, )?; let app_version = &MenuItem::with_id( app_handle, - "app_version", - format!("{} {version}", verge_version_text), + MenuIds::VERGE_VERSION, + format!("{} {version}", &texts.verge_version), true, None::<&str>, )?; let more = &Submenu::with_id_and_items( app_handle, - "more", - more_text, + MenuIds::MORE, + &texts.more, true, &[ close_all_connections, @@ -1045,7 +1134,13 @@ async fn create_tray_menu( ], )?; - let quit = &MenuItem::with_id(app_handle, "quit", exit_text, true, Some("CmdOrControl+Q"))?; + let quit = &MenuItem::with_id( + app_handle, + MenuIds::EXIT, + &texts.exit, + true, + Some("CmdOrControl+Q"), + )?; let separator = &PredefinedMenuItem::separator(app_handle)?; @@ -1063,9 +1158,9 @@ async fn create_tray_menu( // 如果有代理节点,添加代理节点菜单 if show_proxy_groups_inline { if !inline_proxy_items.is_empty() { - menu_items.extend_from_slice(&inline_proxy_items); + menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref())); } - } else if let Some(ref proxies_menu) = proxies_submenu { + } else if let Some(ref proxies_menu) = proxies_menu { menu_items.push(proxies_menu); } @@ -1074,7 +1169,7 @@ async fn create_tray_menu( system_proxy as &dyn IsMenuItem, tun_mode as &dyn IsMenuItem, separator, - lighteweight_mode as &dyn IsMenuItem, + lightweight_mode as &dyn IsMenuItem, copy_env as &dyn IsMenuItem, open_dir as &dyn IsMenuItem, more as &dyn IsMenuItem, @@ -1091,13 +1186,14 @@ async fn create_tray_menu( fn on_menu_event(_: &AppHandle, event: MenuEvent) { AsyncHandler::spawn(|| async move { match event.id.as_ref() { - mode @ ("rule_mode" | "global_mode" | "direct_mode") => { - let mode = &mode[0..mode.len() - 5]; // Removing the "_mode" suffix + mode @ (MenuIds::RULE_MODE | MenuIds::GLOBAL_MODE | MenuIds::DIRECT_MODE) => { + // Removing the the "tray_" preffix and "_mode" suffix + let mode = &mode[5..mode.len() - 5]; logging!(info, Type::ProxyMode, "Switch Proxy Mode To: {}", mode); feat::change_clash_mode(mode.into()).await; } - "open_window" => { - log::info!(target: "app", "托盘菜单点击: 打开窗口"); + MenuIds::DASHBOARD => { + logging!(info, Type::Tray, "托盘菜单点击: 打开窗口"); if !should_handle_tray_click() { return; @@ -1106,40 +1202,51 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { WindowManager::show_main_window().await; }; } - "system_proxy" => { + MenuIds::SYSTEM_PROXY => { feat::toggle_system_proxy().await; } - "tun_mode" => { + MenuIds::TUN_MODE => { feat::toggle_tun_mode(None).await; } - "close_all_connections" => { + MenuIds::CLOSE_ALL_CONNECTIONS => { if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { - log::error!(target: "app", "Failed to close all connections from tray: {err}"); + logging!( + error, + Type::Tray, + "Failed to close all connections from tray: {err}" + ); } } - "copy_env" => feat::copy_clash_env().await, - "open_app_dir" => { + MenuIds::COPY_ENV => feat::copy_clash_env().await, + MenuIds::CONF_DIR => { + println!("Open directory submenu clicked"); let _ = cmd::open_app_dir().await; } - "open_core_dir" => { + MenuIds::CORE_DIR => { let _ = cmd::open_core_dir().await; } - "open_logs_dir" => { + MenuIds::LOGS_DIR => { let _ = cmd::open_logs_dir().await; } - "restart_clash" => feat::restart_clash_core().await, - "restart_app" => feat::restart_app().await, - "entry_lightweight_mode" => { + MenuIds::APP_LOG => { + let _ = cmd::open_app_log().await; + } + MenuIds::CORE_LOG => { + let _ = cmd::open_core_log().await; + } + MenuIds::RESTART_CLASH => feat::restart_clash_core().await, + MenuIds::RESTART_APP => feat::restart_app().await, + MenuIds::LIGHTWEIGHT_MODE => { if !should_handle_tray_click() { return; } if !is_in_lightweight_mode() { - lightweight::entry_lightweight_mode().await; // Await async function + lightweight::entry_lightweight_mode().await; } else { - lightweight::exit_lightweight_mode().await; // Await async function + lightweight::exit_lightweight_mode().await; } } - "quit" => { + MenuIds::EXIT => { feat::quit().await; } id if id.starts_with("profiles_") => { @@ -1160,12 +1267,25 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { .await { Ok(_) => { - log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name); + logging!( + info, + Type::Tray, + "切换代理成功: {} -> {}", + group_name, + proxy_name + ); let _ = handle::Handle::app_handle() .emit("verge://refresh-proxy-config", ()); } Err(e) => { - log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e); + logging!( + error, + Type::Tray, + "切换代理失败: {} -> {}, 错误: {:?}", + group_name, + proxy_name, + e + ); // Fallback to IPC update if (handle::Handle::mihomo() @@ -1174,7 +1294,13 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { .await) .is_ok() { - log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name); + logging!( + info, + Type::Tray, + "代理切换回退成功: {} -> {}", + group_name, + proxy_name + ); let app_handle = handle::Handle::app_handle(); let _ = app_handle.emit("verge://force-refresh-proxies", ()); @@ -1188,7 +1314,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { // Ensure tray state update is awaited and properly handled if let Err(e) = Tray::global().update_all_states().await { - log::warn!(target: "app", "更新托盘状态失败: {e}"); + logging!(warn, Type::Tray, "Failed to update tray state: {e}"); } }); } diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index 777d6505..204497a0 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -1,9 +1,9 @@ use anyhow::Result; +use scopeguard::defer; use smartstring::alias::String; -use std::path::Path; -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use tauri_plugin_shell::ShellExt; -use tokio::sync::Mutex; +use tokio::fs; use crate::config::{Config, ConfigType}; use crate::core::handle; @@ -11,44 +11,38 @@ use crate::singleton_lazy; use crate::utils::dirs; use crate::{logging, utils::logging::Type}; -// pub enum ValidationResult { -// Valid, -// Invalid(String), -// } - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum ValidationProcessStatus { - Ongoing, - Completed, -} - pub struct CoreConfigValidator { - // inner: Vec, - // result: ValidationResult, - process_status: Arc>, + is_processing: AtomicBool, } impl CoreConfigValidator { pub fn new() -> Self { - CoreConfigValidator { - process_status: Arc::new(Mutex::new(ValidationProcessStatus::Completed)), + Self { + is_processing: AtomicBool::new(false), } } + pub fn try_start(&self) -> bool { + !self.is_processing.swap(true, Ordering::AcqRel) + } + + pub fn finish(&self) { + self.is_processing.store(false, Ordering::Release) + } +} + +impl CoreConfigValidator { /// 检查文件是否为脚本文件 - fn is_script_file

(path: P) -> Result - where - P: AsRef + std::fmt::Display, - { + async fn is_script_file(path: &str) -> Result { // 1. 先通过扩展名快速判断 - if has_ext(&path, "yaml") || has_ext(&path, "yml") { + if has_ext(path, "yaml") || has_ext(path, "yml") { return Ok(false); // YAML文件不是脚本文件 - } else if has_ext(&path, "js") { + } else if has_ext(path, "js") { return Ok(true); // JS文件是脚本文件 } // 2. 读取文件内容 - let content = match std::fs::read_to_string(&path) { + let content = match fs::read_to_string(path).await { Ok(content) => content, Err(err) => { logging!( @@ -118,11 +112,11 @@ impl CoreConfigValidator { } /// 只进行文件语法检查,不进行完整验证 - fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> { + async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> { logging!(info, Type::Validate, "开始检查文件: {}", config_path); // 读取文件内容 - let content = match std::fs::read_to_string(config_path) { + let content = match fs::read_to_string(config_path).await { Ok(content) => content, Err(err) => { let error_msg = format!("Failed to read file: {err}").into(); @@ -147,9 +141,9 @@ impl CoreConfigValidator { } /// 验证脚本文件语法 - fn validate_script_file(path: &str) -> Result<(bool, String)> { + async fn validate_script_file(path: &str) -> Result<(bool, String)> { // 读取脚本内容 - let content = match std::fs::read_to_string(path) { + let content = match fs::read_to_string(path).await { Ok(content) => content, Err(err) => { let error_msg = format!("Failed to read script file: {err}").into(); @@ -219,14 +213,14 @@ impl CoreConfigValidator { "检测到Merge文件,仅进行语法检查: {}", config_path ); - return Self::validate_file_syntax(config_path); + return Self::validate_file_syntax(config_path).await; } // 检查是否为脚本文件 let is_script = if config_path.ends_with(".js") { true } else { - match Self::is_script_file(config_path) { + match Self::is_script_file(config_path).await { Ok(result) => result, Err(err) => { // 如果无法确定文件类型,尝试使用Clash内核验证 @@ -249,7 +243,7 @@ impl CoreConfigValidator { "检测到脚本文件,使用JavaScript验证: {}", config_path ); - return Self::validate_script_file(config_path); + return Self::validate_script_file(config_path).await; } // 对YAML配置文件使用Clash内核验证 @@ -325,22 +319,18 @@ impl CoreConfigValidator { /// 验证运行时配置 pub async fn validate_config(&self) -> Result<(bool, String)> { - if *self.process_status.lock().await == ValidationProcessStatus::Ongoing { + if !self.try_start() { logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求"); return Ok((true, String::new())); } - *self.process_status.lock().await = ValidationProcessStatus::Ongoing; + defer! { + self.finish(); + } logging!(info, Type::Validate, "生成临时配置文件用于验证"); - let result = async { - let config_path = Config::generate_file(ConfigType::Check).await?; - let config_path = dirs::path_to_str(&config_path)?; - Self::validate_config_internal(config_path).await - } - .await; - - *self.process_status.lock().await = ValidationProcessStatus::Completed; - result + let config_path = Config::generate_file(ConfigType::Check).await?; + let config_path = dirs::path_to_str(&config_path)?; + Self::validate_config_internal(config_path).await } } diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 8238061c..a9a81ac8 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -5,7 +5,7 @@ use crate::{ }; use serde_yaml_ng::Mapping; use smartstring::alias::String; -use std::fs; +use tokio::fs; #[derive(Debug, Clone)] pub struct ChainItem { @@ -83,7 +83,7 @@ impl AsyncChainItemFrom for Option { match itype { "script" => Some(ChainItem { uid, - data: ChainType::Script(fs::read_to_string(path).ok()?.into()), + data: ChainType::Script(fs::read_to_string(path).await.ok()?.into()), }), "merge" => Some(ChainItem { uid, diff --git a/src-tauri/src/enhance/merge.rs b/src-tauri/src/enhance/merge.rs index 0210851d..3d0e4bf9 100644 --- a/src-tauri/src/enhance/merge.rs +++ b/src-tauri/src/enhance/merge.rs @@ -1,3 +1,5 @@ +use crate::{logging, utils::logging::Type}; + use super::use_lowercase; use serde_yaml_ng::{self, Mapping, Value}; @@ -19,7 +21,11 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping { deep_merge(&mut config, &Value::from(merge)); config.as_mapping().cloned().unwrap_or_else(|| { - log::error!("Failed to convert merged config to mapping, using empty mapping"); + logging!( + error, + Type::Core, + "Failed to convert merged config to mapping, using empty mapping" + ); Mapping::new() }) } diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 704f199f..e9183170 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -6,17 +6,45 @@ pub mod seq; mod tun; use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*}; +use crate::constants; +use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; +use crate::{logging, utils::logging::Type}; use serde_yaml_ng::Mapping; use smartstring::alias::String; use std::collections::{HashMap, HashSet}; +use tokio::fs; type ResultLog = Vec<(String, String)>; +#[derive(Debug)] +struct ConfigValues { + clash_config: Mapping, + clash_core: Option, + enable_tun: bool, + enable_builtin: bool, + socks_enabled: bool, + http_enabled: bool, + enable_dns_settings: bool, + #[cfg(not(target_os = "windows"))] + redir_enabled: bool, + #[cfg(target_os = "linux")] + tproxy_enabled: bool, +} -/// Enhance mode -/// 返回最终订阅、该订阅包含的键、和script执行的结果 -pub async fn enhance() -> (Mapping, Vec, HashMap) { - // config.yaml 的订阅 +#[derive(Debug)] +struct ProfileItems { + config: Mapping, + merge_item: ChainItem, + script_item: ChainItem, + rules_item: ChainItem, + proxies_item: ChainItem, + groups_item: ChainItem, + global_merge: ChainItem, + global_script: ChainItem, + profile_name: String, +} + +async fn get_config_values() -> ConfigValues { let clash_config = { Config::clash().await.latest_ref().0.clone() }; let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { @@ -31,12 +59,14 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { verge.enable_dns_settings.unwrap_or(false), ) }; + #[cfg(not(target_os = "windows"))] let redir_enabled = { let verge = Config::verge().await; let verge = verge.latest_ref(); verge.verge_redir_enabled.unwrap_or(false) }; + #[cfg(target_os = "linux")] let tproxy_enabled = { let verge = Config::verge().await; @@ -44,9 +74,189 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { verge.verge_tproxy_enabled.unwrap_or(false) }; + ConfigValues { + clash_config, + clash_core, + enable_tun, + enable_builtin, + socks_enabled, + http_enabled, + enable_dns_settings, + #[cfg(not(target_os = "windows"))] + redir_enabled, + #[cfg(target_os = "linux")] + tproxy_enabled, + } +} + +async fn collect_profile_items() -> ProfileItems { // 从profiles里拿东西 - 先收集需要的数据,然后释放锁 let ( - mut config, + current, + merge_uid, + script_uid, + rules_uid, + proxies_uid, + groups_uid, + _current_profile_uid, + name, + ) = { + let current = { + let profiles = Config::profiles().await; + let profiles_clone = profiles.latest_ref().clone(); + profiles_clone.current_mapping().await.unwrap_or_default() + }; + + let profiles = Config::profiles().await; + let profiles_ref = profiles.latest_ref(); + + let merge_uid = profiles_ref.current_merge().unwrap_or_default(); + let script_uid = profiles_ref.current_script().unwrap_or_default(); + let rules_uid = profiles_ref.current_rules().unwrap_or_default(); + let proxies_uid = profiles_ref.current_proxies().unwrap_or_default(); + let groups_uid = profiles_ref.current_groups().unwrap_or_default(); + let current_profile_uid = profiles_ref.get_current().unwrap_or_default(); + + let name = profiles_ref + .get_item(¤t_profile_uid) + .ok() + .and_then(|item| item.name.clone()) + .unwrap_or_default(); + + ( + current, + merge_uid, + script_uid, + rules_uid, + proxies_uid, + groups_uid, + current_profile_uid, + name, + ) + }; + + // 现在获取具体的items,此时profiles锁已经释放 + let merge_item = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(merge_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Merge(Mapping::new()), + }); + + let script_item = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(script_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), + }); + + let rules_item = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(rules_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Rules(SeqMap::default()), + }); + + let proxies_item = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(proxies_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Proxies(SeqMap::default()), + }); + + let groups_item = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item(groups_uid).ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "".into(), + data: ChainType::Groups(SeqMap::default()), + }); + + let global_merge = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item("Merge").ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "Merge".into(), + data: ChainType::Merge(Mapping::new()), + }); + + let global_script = { + let item = { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + profiles.get_item("Script").ok().cloned() + }; + if let Some(item) = item { + >::from_async(&item).await + } else { + None + } + } + .unwrap_or_else(|| ChainItem { + uid: "Script".into(), + data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), + }); + + ProfileItems { + config: current, merge_item, script_item, rules_item, @@ -54,192 +264,19 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { groups_item, global_merge, global_script, - profile_name, - ) = { - // 收集所有需要的数据,然后释放profiles锁 - let ( - current, - merge_uid, - script_uid, - rules_uid, - proxies_uid, - groups_uid, - _current_profile_uid, - name, - ) = { - // 分离async调用和数据获取,避免借用检查问题 - let current = { - let profiles = Config::profiles().await; - let profiles_clone = profiles.latest_ref().clone(); - profiles_clone.current_mapping().await.unwrap_or_default() - }; + profile_name: name, + } +} - // 重新获取锁进行其他操作 - let profiles = Config::profiles().await; - let profiles_ref = profiles.latest_ref(); +fn process_global_items( + mut config: Mapping, + global_merge: ChainItem, + global_script: ChainItem, + profile_name: String, +) -> (Mapping, Vec, HashMap) { + let mut result_map = HashMap::new(); + let mut exists_keys = use_keys(&config); - let merge_uid = profiles_ref.current_merge().unwrap_or_default(); - let script_uid = profiles_ref.current_script().unwrap_or_default(); - let rules_uid = profiles_ref.current_rules().unwrap_or_default(); - let proxies_uid = profiles_ref.current_proxies().unwrap_or_default(); - let groups_uid = profiles_ref.current_groups().unwrap_or_default(); - let current_profile_uid = profiles_ref.get_current().unwrap_or_default(); - - let name = profiles_ref - .get_item(¤t_profile_uid) - .ok() - .and_then(|item| item.name.clone()) - .unwrap_or_default(); - - ( - current, - merge_uid, - script_uid, - rules_uid, - proxies_uid, - groups_uid, - current_profile_uid, - name, - ) - }; - - // 现在获取具体的items,此时profiles锁已经释放 - let merge = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&merge_uid).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Merge(Mapping::new()), - }); - - let script = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&script_uid).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - }); - - let rules = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&rules_uid).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Rules(SeqMap::default()), - }); - - let proxies = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&proxies_uid).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Proxies(SeqMap::default()), - }); - - let groups = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&groups_uid).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "".into(), - data: ChainType::Groups(SeqMap::default()), - }); - - let global_merge = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&"Merge".into()).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "Merge".into(), - data: ChainType::Merge(Mapping::new()), - }); - - let global_script = { - let item = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - profiles.get_item(&"Script".into()).ok().cloned() - }; - if let Some(item) = item { - >::from_async(&item).await - } else { - None - } - } - .unwrap_or_else(|| ChainItem { - uid: "Script".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - }); - - ( - current, - merge, - script, - rules, - proxies, - groups, - global_merge, - global_script, - name, - ) - }; - - let mut result_map = HashMap::new(); // 保存脚本日志 - let mut exists_keys = use_keys(&config); // 保存出现过的keys - - // 全局Merge和Script if let ChainType::Merge(merge) = global_merge.data { exists_keys.extend(use_keys(&merge)); config = use_merge(merge, config.to_owned()); @@ -247,7 +284,6 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { if let ChainType::Script(script) = global_script.data { let mut logs = vec![]; - match use_script(script, config.to_owned(), profile_name.to_owned()) { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); @@ -256,11 +292,24 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { } Err(err) => logs.push(("exception".into(), err.to_string().into())), } - result_map.insert(global_script.uid, logs); } - // 订阅关联的Merge、Script、Rules、Proxies、Groups + (config, exists_keys, result_map) +} + +#[allow(clippy::too_many_arguments)] +fn process_profile_items( + mut config: Mapping, + mut exists_keys: Vec, + mut result_map: HashMap, + rules_item: ChainItem, + proxies_item: ChainItem, + groups_item: ChainItem, + merge_item: ChainItem, + script_item: ChainItem, + profile_name: String, +) -> (Mapping, Vec, HashMap) { if let ChainType::Rules(rules) = rules_item.data { config = use_seq(rules, config.to_owned(), "rules"); } @@ -280,7 +329,6 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { if let ChainType::Script(script) = script_item.data { let mut logs = vec![]; - match use_script(script, config.to_owned(), profile_name) { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); @@ -289,11 +337,20 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { } Err(err) => logs.push(("exception".into(), err.to_string().into())), } - result_map.insert(script_item.uid, logs); } - // 合并默认的config + (config, exists_keys, result_map) +} + +async fn merge_default_config( + mut config: Mapping, + clash_config: Mapping, + socks_enabled: bool, + http_enabled: bool, + #[cfg(not(target_os = "windows"))] redir_enabled: bool, + #[cfg(target_os = "linux")] tproxy_enabled: bool, +) -> Mapping { for (key, value) in clash_config.into_iter() { if key.as_str() == Some("tun") { let mut tun = config.get_mut("tun").map_or_else(Mapping::new, |val| { @@ -353,66 +410,140 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { } } - // 内建脚本最后跑 + config +} + +fn apply_builtin_scripts( + mut config: Mapping, + clash_core: Option, + enable_builtin: bool, +) -> Mapping { if enable_builtin { ChainItem::builtin() .into_iter() .filter(|(s, _)| s.is_support(clash_core.as_ref())) .map(|(_, c)| c) .for_each(|item| { - log::debug!(target: "app", "run builtin script {}", item.uid); + logging!(debug, Type::Core, "run builtin script {}", item.uid); if let ChainType::Script(script) = item.data { match use_script(script, config.to_owned(), "".into()) { Ok((res_config, _)) => { config = res_config; } Err(err) => { - log::error!(target: "app", "builtin script error `{err}`"); + logging!(error, Type::Core, "builtin script error `{err}`"); } } } }); } - config = use_tun(config, enable_tun); - config = use_sort(config); + config +} - // 应用独立的DNS配置(如果启用) - if enable_dns_settings { - use crate::utils::dirs; - use std::fs; +async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping { + if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() { + let dns_path = app_dir.join(constants::files::DNS_CONFIG); - if let Ok(app_dir) = dirs::app_home_dir() { - let dns_path = app_dir.join("dns_config.yaml"); - - if dns_path.exists() - && let Ok(dns_yaml) = fs::read_to_string(&dns_path) - && let Ok(dns_config) = serde_yaml_ng::from_str::(&dns_yaml) + if dns_path.exists() + && let Ok(dns_yaml) = fs::read_to_string(&dns_path).await + && let Ok(dns_config) = serde_yaml_ng::from_str::(&dns_yaml) + { + if let Some(hosts_value) = dns_config.get("hosts") + && hosts_value.is_mapping() { - // 处理hosts配置 - if let Some(hosts_value) = dns_config.get("hosts") - && hosts_value.is_mapping() - { - config.insert("hosts".into(), hosts_value.clone()); - log::info!(target: "app", "apply hosts configuration"); - } + config.insert("hosts".into(), hosts_value.clone()); + logging!(info, Type::Core, "apply hosts configuration"); + } - if let Some(dns_value) = dns_config.get("dns") { - if let Some(dns_mapping) = dns_value.as_mapping() { - config.insert("dns".into(), dns_mapping.clone().into()); - log::info!(target: "app", "apply dns_config.yaml (dns section)"); - } - } else { - config.insert("dns".into(), dns_config.into()); - log::info!(target: "app", "apply dns_config.yaml"); + if let Some(dns_value) = dns_config.get("dns") { + if let Some(dns_mapping) = dns_value.as_mapping() { + config.insert("dns".into(), dns_mapping.clone().into()); + logging!(info, Type::Core, "apply dns_config.yaml (dns section)"); } + } else { + config.insert("dns".into(), dns_config.into()); + logging!(info, Type::Core, "apply dns_config.yaml"); } } } + config +} + +/// Enhance mode +/// 返回最终订阅、该订阅包含的键、和script执行的结果 +pub async fn enhance() -> (Mapping, Vec, HashMap) { + // gather config values + let cfg_vals = get_config_values().await; + let ConfigValues { + clash_config, + clash_core, + enable_tun, + enable_builtin, + socks_enabled, + http_enabled, + enable_dns_settings, + #[cfg(not(target_os = "windows"))] + redir_enabled, + #[cfg(target_os = "linux")] + tproxy_enabled, + } = cfg_vals; + + // collect profile items + let profile = collect_profile_items().await; + let config = profile.config; + let merge_item = profile.merge_item; + let script_item = profile.script_item; + let rules_item = profile.rules_item; + let proxies_item = profile.proxies_item; + let groups_item = profile.groups_item; + let global_merge = profile.global_merge; + let global_script = profile.global_script; + let profile_name = profile.profile_name; + + // process globals + let (config, exists_keys, result_map) = + process_global_items(config, global_merge, global_script, profile_name.clone()); + + // process profile-specific items + let (config, exists_keys, result_map) = process_profile_items( + config, + exists_keys, + result_map, + rules_item, + proxies_item, + groups_item, + merge_item, + script_item, + profile_name, + ); + + // merge default clash config + let config = merge_default_config( + config, + clash_config, + socks_enabled, + http_enabled, + #[cfg(not(target_os = "windows"))] + redir_enabled, + #[cfg(target_os = "linux")] + tproxy_enabled, + ) + .await; + + // builtin scripts + let mut config = apply_builtin_scripts(config, clash_core, enable_builtin); + + config = use_tun(config, enable_tun); + config = use_sort(config); + + // dns settings + config = apply_dns_settings(config, enable_dns_settings).await; + let mut exists_set = HashSet::new(); exists_set.extend(exists_keys); - exists_keys = exists_set.into_iter().collect(); + let exists_keys: Vec = exists_set.into_iter().collect(); (config, exists_keys, result_map) } diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs index 92c8f018..a09051fa 100644 --- a/src-tauri/src/enhance/tun.rs +++ b/src-tauri/src/enhance/tun.rs @@ -2,6 +2,8 @@ use serde_yaml_ng::{Mapping, Value}; #[cfg(target_os = "macos")] use crate::process::AsyncHandler; +#[cfg(target_os = "linux")] +use crate::{logging, utils::logging::Type}; macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { @@ -42,9 +44,10 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { if should_override { revise!(tun_val, "stack", "mixed"); - log::warn!( - target: "app", - "gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility" + logging!( + warn, + Type::Network, + "Warning: gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility" ); } } diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index e2f0957a..fcc8d048 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -2,6 +2,7 @@ use crate::{ config::{Config, IVerge}, core::backup, logging, logging_error, + process::AsyncHandler, utils::{ dirs::{PathBufExec, app_home_dir, local_backup_dir}, logging::Type, @@ -12,7 +13,8 @@ use chrono::Utc; use reqwest_dav::list_cmd::ListFile; use serde::Serialize; use smartstring::alias::String; -use std::{fs, path::PathBuf}; +use std::path::PathBuf; +use tokio::fs; #[derive(Debug, Serialize)] pub struct LocalBackupFile { @@ -24,7 +26,7 @@ pub struct LocalBackupFile { /// Create a backup and upload to WebDAV pub async fn create_backup_and_upload_webdav() -> Result<()> { - let (file_name, temp_file_path) = backup::create_backup().map_err(|err| { + let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { logging!(error, Type::Backup, "Failed to create backup: {err:#?}"); err })?; @@ -97,12 +99,14 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { })?; // extract zip file - let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?; + let value = backup_storage_path.clone(); + let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; + let mut zip = zip::ZipArchive::new(file)?; zip.extract(app_home_dir()?)?; logging_error!( Type::Backup, super::patch_verge( - IVerge { + &IVerge { webdav_url, webdav_username, webdav_password, @@ -119,7 +123,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { /// Create a backup and save to local storage pub async fn create_local_backup() -> Result<()> { - let (file_name, temp_file_path) = backup::create_backup().map_err(|err| { + let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { logging!( error, Type::Backup, @@ -131,7 +135,7 @@ pub async fn create_local_backup() -> Result<()> { let backup_dir = local_backup_dir()?; let target_path = backup_dir.join(file_name.as_str()); - if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()) { + if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await { logging!( error, Type::Backup, @@ -151,12 +155,12 @@ pub async fn create_local_backup() -> Result<()> { Ok(()) } -fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { +async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { if let Some(parent) = to.parent() { - fs::create_dir_all(parent)?; + fs::create_dir_all(parent).await?; } - match fs::rename(&from, &to) { + match fs::rename(&from, &to).await { Ok(_) => Ok(()), Err(rename_err) => { // Attempt copy + remove as fallback, covering cross-device moves @@ -165,8 +169,11 @@ fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { Type::Backup, "Failed to rename backup file directly, fallback to copy/remove: {rename_err:#?}" ); - fs::copy(&from, &to).map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?; + fs::copy(&from, &to) + .await + .map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?; fs::remove_file(&from) + .await .map_err(|err| anyhow!("Failed to remove temp backup file: {err:#?}"))?; Ok(()) } @@ -174,24 +181,25 @@ fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { } /// List local backups -pub fn list_local_backup() -> Result> { +pub async fn list_local_backup() -> Result> { let backup_dir = local_backup_dir()?; if !backup_dir.exists() { return Ok(vec![]); } let mut backups = Vec::new(); - for entry in fs::read_dir(&backup_dir)? { - let entry = entry?; + let mut dir = fs::read_dir(&backup_dir).await?; + while let Some(entry) = dir.next_entry().await? { let path = entry.path(); - if !path.is_file() { + let metadata = entry.metadata().await?; + if !metadata.is_file() { continue; } - let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { - continue; + let file_name = match path.file_name().and_then(|name| name.to_str()) { + Some(name) => name, + None => continue, }; - let metadata = entry.metadata()?; let last_modified = metadata .modified() .map(|time| chrono::DateTime::::from(time).to_rfc3339()) @@ -239,12 +247,13 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { let webdav_username = verge_data.webdav_username.clone(); let webdav_password = verge_data.webdav_password.clone(); - let mut zip = zip::ZipArchive::new(fs::File::open(&target_path)?)?; + let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??; + let mut zip = zip::ZipArchive::new(file)?; zip.extract(app_home_dir()?)?; logging_error!( Type::Backup, super::patch_verge( - IVerge { + &IVerge { webdav_url, webdav_username, webdav_password, @@ -258,7 +267,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { } /// Export local backup file to user selected destination -pub fn export_local_backup(filename: String, destination: String) -> Result<()> { +pub async fn export_local_backup(filename: String, destination: String) -> Result<()> { let backup_dir = local_backup_dir()?; let source_path = backup_dir.join(filename.as_str()); if !source_path.exists() { @@ -267,10 +276,11 @@ pub fn export_local_backup(filename: String, destination: String) -> Result<()> let dest_path = PathBuf::from(destination.as_str()); if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent)?; + fs::create_dir_all(parent).await?; } fs::copy(&source_path, &dest_path) + .await .map(|_| ()) .map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?; Ok(()) diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index b4143a26..cf031d78 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -1,7 +1,7 @@ use crate::{ config::Config, core::{CoreManager, handle, tray}, - logging_error, + logging, logging_error, process::AsyncHandler, utils::{self, logging::Type, resolve}, }; @@ -17,7 +17,7 @@ pub async fn restart_clash_core() { } Err(err) => { handle::Handle::notice_message("set_config::error", format!("{err}")); - log::error!(target:"app", "{err}"); + logging!(error, Type::Core, "{err}"); } } } @@ -30,7 +30,7 @@ pub async fn restart_app() { "restart_app::error", format!("Failed to cleanup resources: {err}"), ); - log::error!(target:"app", "Restart failed during cleanup: {err}"); + logging!(error, Type::Core, "Restart failed during cleanup: {err}"); return; } @@ -50,7 +50,7 @@ fn after_change_clash_mode() { } } Err(err) => { - log::error!(target: "app", "Failed to get connections: {err}"); + logging!(error, Type::Core, "Failed to get connections: {err}"); } } }); @@ -64,7 +64,7 @@ pub async fn change_clash_mode(mode: String) { let json_value = serde_json::json!({ "mode": mode }); - log::debug!(target: "app", "change clash mode to {mode}"); + logging!(debug, Type::Core, "change clash mode to {mode}"); match handle::Handle::mihomo() .await .patch_base_config(&json_value) @@ -91,7 +91,7 @@ pub async fn change_clash_mode(mode: String) { after_change_clash_mode(); } } - Err(err) => log::error!(target: "app", "{err}"), + Err(err) => logging!(error, Type::Core, "{err}"), } } @@ -123,7 +123,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { match response { Ok(response) => { - log::trace!(target: "app", "test_delay response: {response:#?}"); + logging!(trace, Type::Network, "test_delay response: {response:#?}"); if response.status().is_success() { Ok(start.elapsed().as_millis() as u32) } else { @@ -131,7 +131,7 @@ pub async fn test_delay(url: String) -> anyhow::Result { } } Err(err) => { - log::trace!(target: "app", "test_delay error: {err:#?}"); + logging!(trace, Type::Network, "test_delay error: {err:#?}"); Err(err) } } diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 506f5fa6..1ca9669c 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -63,23 +63,19 @@ enum UpdateFlags { LighteWeight = 1 << 10, } -/// Patch Verge configuration -pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { - Config::verge() - .await - .draft_mut() - .patch_config(patch.clone()); +fn determine_update_flags(patch: &IVerge) -> i32 { + let mut update_flags: i32 = UpdateFlags::None as i32; let tun_mode = patch.enable_tun_mode; let auto_launch = patch.enable_auto_launch; let system_proxy = patch.enable_system_proxy; let pac = patch.proxy_auto_config; - let pac_content = patch.pac_file_content; - let proxy_bypass = patch.system_proxy_bypass; - let language = patch.language; + let pac_content = &patch.pac_file_content; + let proxy_bypass = &patch.system_proxy_bypass; + let language = &patch.language; let mixed_port = patch.verge_mixed_port; #[cfg(target_os = "macos")] - let tray_icon = patch.tray_icon; + let tray_icon = &patch.tray_icon; #[cfg(not(target_os = "macos"))] let tray_icon: Option = None; let common_tray_icon = patch.common_tray_icon; @@ -98,147 +94,156 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { let http_enabled = patch.verge_http_enabled; let http_port = patch.verge_port; let enable_tray_speed = patch.enable_tray_speed; - let enable_tray_icon = patch.enable_tray_icon; + // let enable_tray_icon = patch.enable_tray_icon; let enable_global_hotkey = patch.enable_global_hotkey; - let tray_event = patch.tray_event; + let tray_event = &patch.tray_event; let home_cards = patch.home_cards.clone(); let enable_auto_light_weight = patch.enable_auto_light_weight_mode; let enable_external_controller = patch.enable_external_controller; - let res: std::result::Result<(), anyhow::Error> = { - // Initialize with no flags set - let mut update_flags: i32 = UpdateFlags::None as i32; + let tray_inline_proxy_groups = patch.tray_inline_proxy_groups; - if tun_mode.is_some() { - update_flags |= UpdateFlags::ClashConfig as i32; - update_flags |= UpdateFlags::SystrayMenu as i32; - update_flags |= UpdateFlags::SystrayTooltip as i32; - update_flags |= UpdateFlags::SystrayIcon as i32; - } - if enable_global_hotkey.is_some() || home_cards.is_some() { - update_flags |= UpdateFlags::VergeConfig as i32; - } - #[cfg(not(target_os = "windows"))] - if redir_enabled.is_some() || redir_port.is_some() { - update_flags |= UpdateFlags::RestartCore as i32; - } - #[cfg(target_os = "linux")] - if tproxy_enabled.is_some() || tproxy_port.is_some() { - update_flags |= UpdateFlags::RestartCore as i32; - } - if socks_enabled.is_some() - || http_enabled.is_some() - || socks_port.is_some() - || http_port.is_some() - || mixed_port.is_some() - { - update_flags |= UpdateFlags::RestartCore as i32; - } - if auto_launch.is_some() { - update_flags |= UpdateFlags::Launch as i32; - } + if tun_mode.is_some() { + update_flags |= UpdateFlags::ClashConfig as i32; + update_flags |= UpdateFlags::SystrayMenu as i32; + update_flags |= UpdateFlags::SystrayTooltip as i32; + update_flags |= UpdateFlags::SystrayIcon as i32; + } + if enable_global_hotkey.is_some() || home_cards.is_some() { + update_flags |= UpdateFlags::VergeConfig as i32; + } + #[cfg(not(target_os = "windows"))] + if redir_enabled.is_some() || redir_port.is_some() { + update_flags |= UpdateFlags::RestartCore as i32; + } + #[cfg(target_os = "linux")] + if tproxy_enabled.is_some() || tproxy_port.is_some() { + update_flags |= UpdateFlags::RestartCore as i32; + } + if socks_enabled.is_some() + || http_enabled.is_some() + || socks_port.is_some() + || http_port.is_some() + || mixed_port.is_some() + { + update_flags |= UpdateFlags::RestartCore as i32; + } + if auto_launch.is_some() { + update_flags |= UpdateFlags::Launch as i32; + } - if system_proxy.is_some() { - update_flags |= UpdateFlags::SysProxy as i32; - update_flags |= UpdateFlags::SystrayMenu as i32; - update_flags |= UpdateFlags::SystrayTooltip as i32; - update_flags |= UpdateFlags::SystrayIcon as i32; - } + if system_proxy.is_some() { + update_flags |= UpdateFlags::SysProxy as i32; + update_flags |= UpdateFlags::SystrayMenu as i32; + update_flags |= UpdateFlags::SystrayTooltip as i32; + update_flags |= UpdateFlags::SystrayIcon as i32; + } - if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() { - update_flags |= UpdateFlags::SysProxy as i32; - } + if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() { + update_flags |= UpdateFlags::SysProxy as i32; + } - if language.is_some() { - update_flags |= UpdateFlags::SystrayMenu as i32; - } - if common_tray_icon.is_some() - || sysproxy_tray_icon.is_some() - || tun_tray_icon.is_some() - || tray_icon.is_some() - || enable_tray_speed.is_some() - || enable_tray_icon.is_some() - { - update_flags |= UpdateFlags::SystrayIcon as i32; - } + if language.is_some() { + update_flags |= UpdateFlags::SystrayMenu as i32; + } + if common_tray_icon.is_some() + || sysproxy_tray_icon.is_some() + || tun_tray_icon.is_some() + || tray_icon.is_some() + || enable_tray_speed.is_some() + // || enable_tray_icon.is_some() + { + update_flags |= UpdateFlags::SystrayIcon as i32; + } - if patch.hotkeys.is_some() { - update_flags |= UpdateFlags::Hotkey as i32; - update_flags |= UpdateFlags::SystrayMenu as i32; - } + if patch.hotkeys.is_some() { + update_flags |= UpdateFlags::Hotkey as i32; + update_flags |= UpdateFlags::SystrayMenu as i32; + } - if tray_event.is_some() { - update_flags |= UpdateFlags::SystrayClickBehavior as i32; - } + if tray_event.is_some() { + update_flags |= UpdateFlags::SystrayClickBehavior as i32; + } - if enable_auto_light_weight.is_some() { - update_flags |= UpdateFlags::LighteWeight as i32; - } + if enable_auto_light_weight.is_some() { + update_flags |= UpdateFlags::LighteWeight as i32; + } - // 处理 external-controller 的开关 - if enable_external_controller.is_some() { - update_flags |= UpdateFlags::RestartCore as i32; - } + if enable_external_controller.is_some() { + update_flags |= UpdateFlags::RestartCore as i32; + } - // Process updates based on flags - if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 { - Config::generate().await?; - CoreManager::global().restart_core().await?; - } - if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 { - CoreManager::global().update_config().await?; - handle::Handle::refresh_clash(); - } - if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 { - Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey; - handle::Handle::refresh_verge(); - } - if (update_flags & (UpdateFlags::Launch as i32)) != 0 { - sysopt::Sysopt::global().update_launch().await?; - } - if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 { - sysopt::Sysopt::global().update_sysproxy().await?; - } - if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 - && let Some(hotkeys) = patch.hotkeys - { - hotkey::Hotkey::global().update(hotkeys).await?; - } - if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 { - tray::Tray::global().update_menu().await?; - } - if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { - tray::Tray::global().update_icon().await?; - } - if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { - tray::Tray::global().update_tooltip().await?; - } - if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 { - tray::Tray::global().update_click_behavior().await?; - } - if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 { - if enable_auto_light_weight.unwrap_or(false) { - lightweight::enable_auto_light_weight_mode().await; - } else { - lightweight::disable_auto_light_weight_mode(); - } - } + if tray_inline_proxy_groups.is_some() { + update_flags |= UpdateFlags::SystrayMenu as i32; + } - >::Ok(()) - }; - match res { - Ok(()) => { - Config::verge().await.apply(); - if !not_save_file { - // 分离数据获取和异步调用 - let verge_data = Config::verge().await.data_mut().clone(); - verge_data.save_file().await?; - } + update_flags +} - Ok(()) - } - Err(err) => { - Config::verge().await.discard(); - Err(err) +async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<()> { + // Process updates based on flags + if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 { + Config::generate().await?; + CoreManager::global().restart_core().await?; + } + if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 { + CoreManager::global().update_config().await?; + handle::Handle::refresh_clash(); + } + if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 { + Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey; + handle::Handle::refresh_verge(); + } + if (update_flags & (UpdateFlags::Launch as i32)) != 0 { + sysopt::Sysopt::global().update_launch().await?; + } + if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 { + sysopt::Sysopt::global().update_sysproxy().await?; + } + if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 + && let Some(hotkeys) = &patch.hotkeys + { + hotkey::Hotkey::global().update(hotkeys.to_owned()).await?; + } + if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 { + tray::Tray::global().update_menu().await?; + } + if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { + tray::Tray::global().update_icon().await?; + } + if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { + tray::Tray::global().update_tooltip().await?; + } + if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 { + tray::Tray::global().update_click_behavior().await?; + } + if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 { + if patch.enable_auto_light_weight_mode.unwrap_or(false) { + lightweight::enable_auto_light_weight_mode().await; + } else { + lightweight::disable_auto_light_weight_mode(); } } + Ok(()) +} + +pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { + Config::verge().await.draft_mut().patch_config(patch); + + let update_flags = determine_update_flags(patch); + let process_flag_result: std::result::Result<(), anyhow::Error> = { + process_terminated_flags(update_flags, patch).await?; + Ok(()) + }; + + if let Err(err) = process_flag_result { + Config::verge().await.discard(); + return Err(err); + } + Config::verge().await.apply(); + if !not_save_file { + // 分离数据获取和异步调用 + let verge_data = Config::verge().await.data_ref().clone(); + verge_data.save_file().await?; + } + Ok(()) } diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index 47e8d402..f3832e38 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -18,154 +18,188 @@ pub async fn toggle_proxy_profile(profile_index: String) { } } Err(err) => { - log::error!(target: "app", "{err}"); + logging!(error, Type::Tray, "{err}"); } } } -/// Update a profile -/// If updating current profile, activate it -/// auto_refresh: 是否自动更新配置和刷新前端 -pub async fn update_profile( - uid: String, - option: Option, - auto_refresh: Option, -) -> Result<()> { - logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid); - let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性 +async fn should_update_profile( + uid: &String, + ignore_auto_update: bool, +) -> Result)>> { + let profiles = Config::profiles().await; + let profiles = profiles.latest_ref(); + let item = profiles.get_item(uid)?; + let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); - let url_opt = { - let profiles = Config::profiles().await; - let profiles = profiles.latest_ref(); - let item = profiles.get_item(&uid)?; - let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); - - if !is_remote { - log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新"); - None // 非远程订阅直接更新 - } else if item.url.is_none() { - log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新"); - bail!("failed to get the profile item url"); - } else if !item + if !is_remote { + logging!( + info, + Type::Config, + "[订阅更新] {uid} 不是远程订阅,跳过更新" + ); + Ok(None) + } else if item.url.is_none() { + logging!( + warn, + Type::Config, + "Warning: [订阅更新] {uid} 缺少URL,无法更新" + ); + bail!("failed to get the profile item url"); + } else if !ignore_auto_update + && !item .option .as_ref() .and_then(|o| o.allow_auto_update) .unwrap_or(true) - { - log::info!(target: "app", "[订阅更新] {} 禁止自动更新,跳过更新", uid); - None - } else { - log::info!(target: "app", - "[订阅更新] {} 是远程订阅,URL: {}", - uid, - item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? - ); - Some(( - item.url - .clone() - .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?, - item.option.clone(), - )) - } + { + logging!( + info, + Type::Config, + "[订阅更新] {} 禁止自动更新,跳过更新", + uid + ); + Ok(None) + } else { + logging!( + info, + Type::Config, + "[订阅更新] {} 是远程订阅,URL: {}", + uid, + item.url + .clone() + .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? + ); + Ok(Some(( + item.url + .clone() + .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?, + item.option.clone(), + ))) + } +} + +async fn perform_profile_update( + uid: &String, + url: &String, + opt: Option<&PrfOption>, + option: Option<&PrfOption>, +) -> Result { + logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容"); + let mut merged_opt = PrfOption::merge(opt, option); + let is_current = { + let profiles = Config::profiles().await; + profiles.latest_ref().is_current_profile_index(uid) }; + let profile_name = { + let profiles = Config::profiles().await; + profiles + .latest_ref() + .get_name_by_uid(uid) + .unwrap_or_default() + }; + let mut last_err; - let should_update = match url_opt { + match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { + Ok(mut item) => { + logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功"); + profiles_draft_update_item_safe(uid, &mut item).await?; + return Ok(is_current); + } + Err(err) => { + logging!( + warn, + Type::Config, + "Warning: [订阅更新] 正常更新失败: {err},尝试使用Clash代理更新" + ); + last_err = err; + } + } + + merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(true); + merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(false); + + match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { + Ok(mut item) => { + logging!( + info, + Type::Config, + "[订阅更新] 使用 Clash代理 更新订阅配置成功" + ); + profiles_draft_update_item_safe(uid, &mut item).await?; + handle::Handle::notice_message("update_with_clash_proxy", profile_name); + drop(last_err); + return Ok(is_current); + } + Err(err) => { + logging!( + warn, + Type::Config, + "Warning: [订阅更新] 正常更新失败: {err},尝试使用Clash代理更新" + ); + last_err = err; + } + } + + merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(false); + merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(true); + + match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { + Ok(mut item) => { + logging!( + info, + Type::Config, + "[订阅更新] 使用 系统代理 更新订阅配置成功" + ); + profiles_draft_update_item_safe(uid, &mut item).await?; + handle::Handle::notice_message("update_with_clash_proxy", profile_name); + drop(last_err); + return Ok(is_current); + } + Err(err) => { + logging!( + warn, + Type::Config, + "Warning: [订阅更新] 正常更新失败: {err},尝试使用系统代理更新" + ); + last_err = err; + } + } + + handle::Handle::notice_message( + "update_failed_even_with_clash", + format!("{profile_name} - {last_err}"), + ); + Ok(is_current) +} + +pub async fn update_profile( + uid: &String, + option: Option<&PrfOption>, + auto_refresh: bool, + ignore_auto_update: bool, +) -> Result<()> { + logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid); + let url_opt = should_update_profile(uid, ignore_auto_update).await?; + + let should_refresh = match url_opt { Some((url, opt)) => { - log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容"); - let merged_opt = PrfOption::merge(opt.clone(), option.clone()); - - // 尝试使用正常设置更新 - match PrfItem::from_url(&url, None, None, merged_opt.clone()).await { - Ok(item) => { - log::info!(target: "app", "[订阅更新] 更新订阅配置成功"); - let profiles = Config::profiles().await; - - // 使用Send-safe helper函数 - let result = profiles_draft_update_item_safe(uid.clone(), item).await; - result?; - - let is_current = Some(uid.clone()) == profiles.latest_ref().get_current(); - log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}"); - is_current && auto_refresh - } - Err(err) => { - // 首次更新失败,尝试使用Clash代理 - log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新"); - - // 发送通知 - handle::Handle::notice_message("update_retry_with_clash", uid.clone()); - - // 保存原始代理设置 - let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy); - let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy); - - // 创建使用Clash代理的选项 - let mut fallback_opt = merged_opt.unwrap_or_default(); - fallback_opt.with_proxy = Some(false); - fallback_opt.self_proxy = Some(true); - - // 使用Clash代理重试 - match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await { - Ok(mut item) => { - log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功"); - - // 恢复原始代理设置到item - if let Some(option) = item.option.as_mut() { - option.with_proxy = original_with_proxy; - option.self_proxy = original_self_proxy; - } - - // 更新到配置 - let profiles = Config::profiles().await; - - // 使用 Send-safe 方法进行数据操作 - profiles_draft_update_item_safe(uid.clone(), item.clone()).await?; - - // 获取配置名称用于通知 - let profile_name = item.name.clone().unwrap_or_else(|| uid.clone()); - - // 发送通知告知用户自动更新使用了回退机制 - handle::Handle::notice_message("update_with_clash_proxy", profile_name); - - let is_current = Some(uid.clone()) == profiles.data_ref().get_current(); - log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}"); - is_current && auto_refresh - } - Err(retry_err) => { - log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}"); - handle::Handle::notice_message( - "update_failed_even_with_clash", - format!("{retry_err}"), - ); - return Err(retry_err); - } - } - } - } + perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh } None => auto_refresh, }; - if should_update { + if should_refresh { logging!(info, Type::Config, "[订阅更新] 更新内核配置"); match CoreManager::global().update_config().await { Ok(_) => { logging!(info, Type::Config, "[订阅更新] 更新成功"); handle::Handle::refresh_clash(); - // if let Err(err) = cmd::proxy::force_refresh_proxies().await { - // logging!( - // error, - // Type::Config, - // true, - // "[订阅更新] 代理组刷新失败: {}", - // err - // ); - // } } Err(err) => { logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err); handle::Handle::notice_message("update_failed", format!("{err}")); - log::error!(target: "app", "{err}"); + logging!(error, Type::Config, "{err}"); } } } diff --git a/src-tauri/src/feat/proxy.rs b/src-tauri/src/feat/proxy.rs index ef063026..731cec20 100644 --- a/src-tauri/src/feat/proxy.rs +++ b/src-tauri/src/feat/proxy.rs @@ -1,35 +1,32 @@ use crate::{ config::{Config, IVerge}, core::handle, + logging, + utils::logging::Type, }; use std::env; use tauri_plugin_clipboard_manager::ClipboardExt; /// Toggle system proxy on/off pub async fn toggle_system_proxy() { - // 获取当前系统代理状态 - let enable = { - let verge = Config::verge().await; - - verge.latest_ref().enable_system_proxy.unwrap_or(false) - }; - // 获取自动关闭连接设置 - let auto_close_connection = { - let verge = Config::verge().await; - - verge.latest_ref().auto_close_connection.unwrap_or(false) - }; + let verge = Config::verge().await; + let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false); + let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false); // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 if enable && auto_close_connection && let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { - log::error!(target: "app", "Failed to close all connections: {err}"); + logging!( + error, + Type::ProxyMode, + "Failed to close all connections: {err}" + ); } let patch_result = super::patch_verge( - IVerge { + &IVerge { enable_system_proxy: Some(!enable), ..IVerge::default() }, @@ -39,7 +36,7 @@ pub async fn toggle_system_proxy() { match patch_result { Ok(_) => handle::Handle::refresh_verge(), - Err(err) => log::error!(target: "app", "{err}"), + Err(err) => logging!(error, Type::ProxyMode, "{err}"), } } @@ -49,7 +46,7 @@ pub async fn toggle_tun_mode(not_save_file: Option) { let enable = enable.unwrap_or(false); match super::patch_verge( - IVerge { + &IVerge { enable_tun_mode: Some(!enable), ..IVerge::default() }, @@ -58,7 +55,7 @@ pub async fn toggle_tun_mode(not_save_file: Option) { .await { Ok(_) => handle::Handle::refresh_verge(), - Err(err) => log::error!(target: "app", "{err}"), + Err(err) => logging!(error, Type::ProxyMode, "{err}"), } } @@ -113,12 +110,16 @@ pub async fn copy_clash_env() { } "fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"), _ => { - log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"); + logging!( + error, + Type::ProxyMode, + "copy_clash_env: Invalid env type! {env_type}" + ); return; } }; if cliboard.write_text(export_text).is_err() { - log::error!(target: "app", "Failed to write to clipboard"); + logging!(error, Type::ProxyMode, "Failed to write to clipboard"); } } diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 57cfc810..a23a41f2 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -14,7 +14,7 @@ pub async fn open_or_close_dashboard() { async fn open_or_close_dashboard_internal() { let _ = lightweight::exit_lightweight_mode().await; let result = WindowManager::toggle_main_window().await; - log::info!(target: "app", "Window toggle result: {result:?}"); + logging!(info, Type::Window, "Window toggle result: {result:?}"); } pub async fn quit() { @@ -47,7 +47,7 @@ pub async fn clean_async() -> bool { let tun_task = async { let tun_enabled = Config::verge() .await - .data_ref() + .latest_ref() .enable_tun_mode .unwrap_or(false); @@ -71,16 +71,20 @@ pub async fn clean_async() -> bool { .await { Ok(Ok(_)) => { - log::info!(target: "app", "TUN模式已禁用"); + logging!(info, Type::Window, "TUN模式已禁用"); true } Ok(Err(e)) => { - log::warn!(target: "app", "禁用TUN模式失败: {e}"); + logging!(warn, Type::Window, "Warning: 禁用TUN模式失败: {e}"); // 超时不阻塞退出 true } Err(_) => { - log::warn!(target: "app", "禁用TUN模式超时(可能系统正在关机),继续退出流程"); + logging!( + warn, + Type::Window, + "Warning: 禁用TUN模式超时(可能系统正在关机),继续退出流程" + ); true } } @@ -101,7 +105,7 @@ pub async fn clean_async() -> bool { .unwrap_or(false); if !sys_proxy_enabled { - log::info!(target: "app", "系统代理未启用,跳过重置"); + logging!(info, Type::Window, "系统代理未启用,跳过重置"); return true; } @@ -110,19 +114,23 @@ pub async fn clean_async() -> bool { if is_shutting_down { // sysproxy-rs 操作注册表(避免.exe的dll错误) - log::info!(target: "app", "检测到正在关机,syspro-rs操作注册表关闭系统代理"); + logging!( + info, + Type::Window, + "检测到正在关机,syspro-rs操作注册表关闭系统代理" + ); match Sysproxy::get_system_proxy() { Ok(mut sysproxy) => { sysproxy.enable = false; if let Err(e) = sysproxy.set_system_proxy() { - log::warn!(target: "app", "关机时关闭系统代理失败: {e}"); + logging!(warn, Type::Window, "Warning: 关机时关闭系统代理失败: {e}"); } else { - log::info!(target: "app", "系统代理已关闭(通过注册表)"); + logging!(info, Type::Window, "系统代理已关闭(通过注册表)"); } } Err(e) => { - log::warn!(target: "app", "关机时获取代理设置失败: {e}"); + logging!(warn, Type::Window, "Warning: 关机时获取代理设置失败: {e}"); } } @@ -136,7 +144,7 @@ pub async fn clean_async() -> bool { } // 正常退出:使用 sysproxy.exe 重置代理 - log::info!(target: "app", "sysproxy.exe重置系统代理"); + logging!(info, Type::Window, "sysproxy.exe重置系统代理"); match timeout( Duration::from_secs(2), @@ -145,15 +153,19 @@ pub async fn clean_async() -> bool { .await { Ok(Ok(_)) => { - log::info!(target: "app", "系统代理已重置"); + logging!(info, Type::Window, "系统代理已重置"); true } Ok(Err(e)) => { - log::warn!(target: "app", "重置系统代理失败: {e}"); + logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}"); true } Err(_) => { - log::warn!(target: "app", "重置系统代理超时,继续退出流程"); + logging!( + warn, + Type::Window, + "Warning: 重置系统代理超时,继续退出流程" + ); true } } @@ -169,11 +181,11 @@ pub async fn clean_async() -> bool { .unwrap_or(false); if !sys_proxy_enabled { - log::info!(target: "app", "系统代理未启用,跳过重置"); + logging!(info, Type::Window, "系统代理未启用,跳过重置"); return true; } - log::info!(target: "app", "开始重置系统代理..."); + logging!(info, Type::Window, "开始重置系统代理..."); match timeout( Duration::from_millis(1500), @@ -182,15 +194,15 @@ pub async fn clean_async() -> bool { .await { Ok(Ok(_)) => { - log::info!(target: "app", "系统代理已重置"); + logging!(info, Type::Window, "系统代理已重置"); true } Ok(Err(e)) => { - log::warn!(target: "app", "重置系统代理失败: {e}"); + logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}"); true } Err(_) => { - log::warn!(target: "app", "重置系统代理超时,继续退出"); + logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出"); true } } @@ -206,11 +218,15 @@ pub async fn clean_async() -> bool { match timeout(stop_timeout, CoreManager::global().stop_core()).await { Ok(_) => { - log::info!(target: "app", "core已停止"); + logging!(info, Type::Window, "core已停止"); true } Err(_) => { - log::warn!(target: "app", "停止core超时(可能系统正在关机),继续退出"); + logging!( + warn, + Type::Window, + "Warning: 停止core超时(可能系统正在关机),继续退出" + ); true } } @@ -226,11 +242,11 @@ pub async fn clean_async() -> bool { .await { Ok(_) => { - log::info!(target: "app", "DNS设置已恢复"); + logging!(info, Type::Window, "DNS设置已恢复"); true } Err(_) => { - log::warn!(target: "app", "恢复DNS设置超时"); + logging!(warn, Type::Window, "Warning: 恢复DNS设置超时"); false } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5942ac15..c6db41c0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,9 @@ mod feat; mod module; mod process; pub mod utils; +use crate::constants::files; +#[cfg(target_os = "macos")] +use crate::module::lightweight; #[cfg(target_os = "linux")] use crate::utils::linux; #[cfg(target_os = "macos")] @@ -19,6 +22,7 @@ use crate::{ process::AsyncHandler, utils::{resolve, server}, }; +use anyhow::Result; use config::Config; use once_cell::sync::OnceCell; use tauri::{AppHandle, Manager}; @@ -28,11 +32,8 @@ use tauri_plugin_deep_link::DeepLinkExt; use utils::logging::Type; pub static APP_HANDLE: OnceCell = OnceCell::new(); - /// Application initialization helper functions mod app_init { - use anyhow::Result; - use super::*; /// Initialize singleton monitoring for other instances @@ -91,14 +92,14 @@ mod app_init { } app.deep_link().on_open_url(|event| { - let url = event.urls().first().map(|u| u.to_string()); - if let Some(url) = url { - AsyncHandler::spawn(|| async { - if let Err(e) = resolve::resolve_scheme(url.into()).await { - logging!(error, Type::Setup, "Failed to resolve scheme: {}", e); - } - }); - } + let urls = event.urls(); + AsyncHandler::spawn(move || async move { + if let Some(url) = urls.first() + && let Err(e) = resolve::resolve_scheme(url.as_ref()).await + { + logging!(error, Type::Setup, "Failed to resolve scheme: {}", e); + } + }); }); Ok(()) @@ -115,7 +116,7 @@ mod app_init { { auto_start_plugin_builder = auto_start_plugin_builder .macos_launcher(MacosLauncher::LaunchAgent) - .app_name(app.config().identifier.clone()); + .app_name(&app.config().identifier); } app.handle().plugin(auto_start_plugin_builder.build())?; Ok(()) @@ -125,7 +126,7 @@ mod app_init { pub fn setup_window_state(app: &tauri::App) -> Result<(), Box> { logging!(info, Type::Setup, "初始化窗口状态管理..."); let window_state_plugin = tauri_plugin_window_state::Builder::new() - .with_filename("window_state.json") + .with_filename(files::WINDOW_STATE) .with_state_flags(tauri_plugin_window_state::StateFlags::default()) .build(); app.handle().plugin(window_state_plugin)?; @@ -141,6 +142,8 @@ mod app_init { cmd::open_logs_dir, cmd::open_web_url, cmd::open_core_dir, + cmd::open_app_log, + cmd::open_core_log, cmd::get_portable_flag, cmd::get_network_interfaces, cmd::get_system_hostname, @@ -285,6 +288,11 @@ pub fn run() { pub async fn handle_reopen(has_visible_windows: bool) { handle::Handle::global().init(); + if lightweight::is_in_lightweight_mode() { + lightweight::exit_lightweight_mode().await; + return; + } + if !has_visible_windows { handle::Handle::global().set_activation_policy_regular(); let _ = WindowManager::show_main_window().await; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f69742fc..4f3d6015 100755 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,6 +10,5 @@ fn main() { std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1"); } } - app_lib::run(); } diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 08ea8575..d5c3900e 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -1,6 +1,6 @@ use crate::{ config::Config, - core::{handle, timer::Timer, tray::Tray}, + core::{handle, timer::Timer}, log_err, logging, process::AsyncHandler, utils::logging::Type, @@ -43,49 +43,42 @@ impl LightweightState { static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8); -static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0); -static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0); - -fn set_state(new: LightweightState) { - LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release); - match new { - LightweightState::Normal => { - logging!(info, Type::Lightweight, "轻量模式已关闭"); - } - LightweightState::In => { - logging!(info, Type::Lightweight, "轻量模式已开启"); - } - LightweightState::Exiting => { - logging!(info, Type::Lightweight, "正在退出轻量模式"); - } - } -} +static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0); +static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0); +#[inline] fn get_state() -> LightweightState { LIGHTWEIGHT_STATE.load(Ordering::Acquire).into() } -// 检查是否处于轻量模式 +#[inline] +fn try_transition(from: LightweightState, to: LightweightState) -> bool { + LIGHTWEIGHT_STATE + .compare_exchange( + from.as_u8(), + to.as_u8(), + Ordering::AcqRel, + Ordering::Relaxed, + ) + .is_ok() +} + +#[inline] +fn record_state_and_log(state: LightweightState) { + LIGHTWEIGHT_STATE.store(state.as_u8(), Ordering::Release); + match state { + LightweightState::Normal => logging!(info, Type::Lightweight, "轻量模式已关闭"), + LightweightState::In => logging!(info, Type::Lightweight, "轻量模式已开启"), + LightweightState::Exiting => logging!(info, Type::Lightweight, "正在退出轻量模式"), + } +} + +#[inline] pub fn is_in_lightweight_mode() -> bool { get_state() == LightweightState::In } -// 设置轻量模式状态(仅 Normal <-> In) -async fn set_lightweight_mode(value: bool) { - let current = get_state(); - if value && current != LightweightState::In { - set_state(LightweightState::In); - } else if !value && current != LightweightState::Normal { - set_state(LightweightState::Normal); - } - - // 只有在状态可用时才触发托盘更新 - if let Err(e) = Tray::global().update_part().await { - log::warn!("Failed to update tray: {e}"); - } -} - -pub async fn run_once_auto_lightweight() { +pub async fn auto_lightweight_boot() -> Result<()> { let verge_config = Config::verge().await; let enable_auto = verge_config .data_mut() @@ -96,39 +89,23 @@ pub async fn run_once_auto_lightweight() { .enable_silent_start .unwrap_or(false); - if !(enable_auto && is_silent_start) { - logging!( - info, - Type::Lightweight, - "不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式" - ); - return; + if is_silent_start { + logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式"); + let _ = entry_lightweight_mode().await; + return Ok(()); } - set_lightweight_mode(true).await; + if !enable_auto { + logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化"); + return Ok(()); + } + + logging!( + info, + Type::Lightweight, + "非静默启动:注册自动轻量模式监听器" + ); enable_auto_light_weight_mode().await; -} - -pub async fn auto_lightweight_mode_init() -> Result<()> { - let is_silent_start = - { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); - let enable_auto = { - Config::verge() - .await - .latest_ref() - .enable_auto_light_weight_mode - } - .unwrap_or(false); - - if enable_auto && !is_silent_start { - logging!( - info, - Type::Lightweight, - "非静默启动直接挂载自动进入轻量模式监听器!" - ); - set_state(LightweightState::Normal); - enable_auto_light_weight_mode().await; - } Ok(()) } @@ -151,43 +128,18 @@ pub fn disable_auto_light_weight_mode() { } pub async fn entry_lightweight_mode() -> bool { - // 尝试从 Normal -> In - if LIGHTWEIGHT_STATE - .compare_exchange( - LightweightState::Normal as u8, - LightweightState::In as u8, - Ordering::Acquire, - Ordering::Relaxed, - ) - .is_err() - { + if !try_transition(LightweightState::Normal, LightweightState::In) { logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用"); return false; } - + record_state_and_log(LightweightState::In); WindowManager::destroy_main_window(); - - set_lightweight_mode(true).await; let _ = cancel_light_weight_timer(); - - // 回到 In - set_state(LightweightState::In); - true } -// 添加从轻量模式恢复的函数 pub async fn exit_lightweight_mode() -> bool { - // 尝试从 In -> Exiting - if LIGHTWEIGHT_STATE - .compare_exchange( - LightweightState::In as u8, - LightweightState::Exiting as u8, - Ordering::Acquire, - Ordering::Relaxed, - ) - .is_err() - { + if !try_transition(LightweightState::In, LightweightState::Exiting) { logging!( info, Type::Lightweight, @@ -195,16 +147,10 @@ pub async fn exit_lightweight_mode() -> bool { ); return false; } - + record_state_and_log(LightweightState::Exiting); WindowManager::show_main_window().await; - - set_lightweight_mode(false).await; let _ = cancel_light_weight_timer(); - - // 回到 Normal - set_state(LightweightState::Normal); - - logging!(info, Type::Lightweight, "轻量模式退出完成"); + record_state_and_log(LightweightState::Normal); true } @@ -215,24 +161,31 @@ pub async fn add_light_weight_timer() { fn setup_window_close_listener() { if let Some(window) = handle::Handle::get_window() { - let handler = window.listen("tauri://close-requested", move |_event| { + let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); + if old_id != 0 { + window.unlisten(old_id); + } + let handler_id = window.listen("tauri://close-requested", move |_event| { std::mem::drop(AsyncHandler::spawn(|| async { if let Err(e) = setup_light_weight_timer().await { - log::warn!("Failed to setup light weight timer: {e}"); + logging!( + warn, + Type::Lightweight, + "Warning: Failed to setup light weight timer: {e}" + ); } })); logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时"); }); - - WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release); + WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release); } } fn cancel_window_close_listener() { if let Some(window) = handle::Handle::get_window() { - let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel); - if handler != 0 { - window.unlisten(handler); + let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); + if id != 0 { + window.unlisten(id); logging!(info, Type::Lightweight, "取消了窗口关闭监听"); } } @@ -240,7 +193,11 @@ fn cancel_window_close_listener() { fn setup_webview_focus_listener() { if let Some(window) = handle::Handle::get_window() { - let handler = window.listen("tauri://focus", move |_event| { + let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); + if old_id != 0 { + window.unlisten(old_id); + } + let handler_id = window.listen("tauri://focus", move |_event| { log_err!(cancel_light_weight_timer()); logging!( info, @@ -248,37 +205,45 @@ fn setup_webview_focus_listener() { "监听到窗口获得焦点,取消轻量模式计时" ); }); - - WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release); + WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release); } } fn cancel_webview_focus_listener() { if let Some(window) = handle::Handle::get_window() { - let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel); - if handler != 0 { - window.unlisten(handler); + let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); + if id != 0 { + window.unlisten(id); logging!(info, Type::Lightweight, "取消了窗口焦点监听"); } } } async fn setup_light_weight_timer() -> Result<()> { - Timer::global().init().await?; + if let Err(e) = Timer::global().init().await { + return Err(e).context("failed to initialize timer"); + } + let once_by_minutes = Config::verge() .await .latest_ref() .auto_light_weight_minutes .unwrap_or(10); - // 获取task_id + { + let timer_map = Timer::global().timer_map.read(); + if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) { + logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建"); + return Ok(()); + } + } + let task_id = { Timer::global() .timer_count .fetch_add(1, std::sync::atomic::Ordering::Relaxed) }; - // 创建任务 let task = TaskBuilder::default() .set_task_id(task_id) .set_maximum_parallel_runnable_num(1) @@ -289,7 +254,6 @@ async fn setup_light_weight_timer() -> Result<()> { }) .context("failed to create timer task")?; - // 添加任务到定时器 { let delay_timer = Timer::global().delay_timer.write(); delay_timer @@ -297,7 +261,6 @@ async fn setup_light_weight_timer() -> Result<()> { .context("failed to add timer task")?; } - // 更新任务映射 { let mut timer_map = Timer::global().timer_map.write(); let timer_task = crate::core::timer::TimerTask { diff --git a/src-tauri/src/process/guard.rs b/src-tauri/src/process/guard.rs index e4a48909..a776094f 100644 --- a/src-tauri/src/process/guard.rs +++ b/src-tauri/src/process/guard.rs @@ -1,13 +1,20 @@ use anyhow::Result; use tauri_plugin_shell::process::CommandChild; +use crate::{logging, utils::logging::Type}; + #[derive(Debug)] pub struct CommandChildGuard(Option); impl Drop for CommandChildGuard { fn drop(&mut self) { if let Err(err) = self.kill() { - log::error!(target: "app", "Failed to kill child process: {}", err); + logging!( + error, + Type::Service, + "Failed to kill child process: {}", + err + ); } } } diff --git a/src-tauri/src/utils/autostart.rs b/src-tauri/src/utils/autostart.rs index 06d360ed..b4dc4965 100644 --- a/src-tauri/src/utils/autostart.rs +++ b/src-tauri/src/utils/autostart.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "windows")] +use crate::{logging, utils::logging::Type}; +#[cfg(target_os = "windows")] use anyhow::{Result, anyhow}; -#[cfg(target_os = "windows")] -use log::info; #[cfg(target_os = "windows")] use std::{os::windows::process::CommandExt, path::Path, path::PathBuf}; @@ -49,15 +49,15 @@ pub async fn create_shortcut() -> Result<()> { .remove_if_exists() .await .inspect(|_| { - info!(target: "app", "成功移除旧启动快捷方式"); + logging!(info, Type::Setup, "成功移除旧启动快捷方式"); }) .inspect_err(|err| { - log::error!(target: "app", "移除旧启动快捷方式失败: {err}"); + logging!(error, Type::Setup, "移除旧启动快捷方式失败: {err}"); }); // 如果新快捷方式已存在,直接返回成功 if new_shortcut_path.exists() { - info!(target: "app", "启动快捷方式已存在"); + logging!(info, Type::Setup, "启动快捷方式已存在"); return Ok(()); } @@ -83,7 +83,7 @@ pub async fn create_shortcut() -> Result<()> { return Err(anyhow!("创建快捷方式失败: {}", error_msg)); } - info!(target: "app", "成功创建启动快捷方式"); + logging!(info, Type::Setup, "成功创建启动快捷方式"); Ok(()) } @@ -102,22 +102,22 @@ pub async fn remove_shortcut() -> Result<()> { .remove_if_exists() .await .inspect(|_| { - info!(target: "app", "成功删除旧启动快捷方式"); + logging!(info, Type::Setup, "成功删除旧启动快捷方式"); removed_any = true; }) .inspect_err(|err| { - log::error!(target: "app", "删除旧启动快捷方式失败: {err}"); + logging!(error, Type::Setup, "删除旧启动快捷方式失败: {err}"); }); let _ = new_shortcut_path .remove_if_exists() .await .inspect(|_| { - info!(target: "app", "成功删除启动快捷方式"); + logging!(info, Type::Setup, "成功删除启动快捷方式"); removed_any = true; }) .inspect_err(|err| { - log::error!(target: "app", "删除启动快捷方式失败: {err}"); + logging!(error, Type::Setup, "删除启动快捷方式失败: {err}"); }); Ok(()) diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 3730be82..18ea6fac 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -1,4 +1,8 @@ -use crate::{core::handle, logging, utils::logging::Type}; +use crate::{ + core::{CoreManager, handle, manager::RunningMode}, + logging, + utils::logging::Type, +}; use anyhow::Result; use async_trait::async_trait; use once_cell::sync::OnceCell; @@ -20,7 +24,6 @@ pub static PORTABLE_FLAG: OnceCell = OnceCell::new(); pub static CLASH_CONFIG: &str = "config.yaml"; pub static VERGE_CONFIG: &str = "verge.yaml"; pub static PROFILE_YAML: &str = "profiles.yaml"; -pub static DNS_CONFIG: &str = "dns_config.yaml"; /// init portable flag pub fn init_portable_flag() -> Result<()> { @@ -58,7 +61,11 @@ pub fn app_home_dir() -> Result { match app_handle.path().data_dir() { Ok(dir) => Ok(dir.join(APP_ID)), Err(e) => { - log::error!(target: "app", "Failed to get the app home directory: {e}"); + logging!( + error, + Type::File, + "Failed to get the app home directory: {e}" + ); Err(anyhow::anyhow!("Failed to get the app homedirectory")) } } @@ -72,7 +79,11 @@ pub fn app_resources_dir() -> Result { match app_handle.path().resource_dir() { Ok(dir) => Ok(dir.join("resources")), Err(e) => { - log::error!(target: "app", "Failed to get the resource directory: {e}"); + logging!( + error, + Type::File, + "Failed to get the resource directory: {e}" + ); Err(anyhow::anyhow!("Failed to get the resource directory")) } } @@ -122,6 +133,11 @@ pub fn app_logs_dir() -> Result { Ok(app_home_dir()?.join("logs")) } +// latest verge log +pub fn app_latest_log() -> Result { + Ok(app_logs_dir()?.join("latest.log")) +} + /// local backups dir pub fn local_backup_dir() -> Result { let dir = app_home_dir()?.join(BACKUP_DIR); @@ -167,6 +183,15 @@ pub fn service_log_dir() -> Result { Ok(log_dir) } +pub fn clash_latest_log() -> Result { + match *CoreManager::global().get_running_mode() { + RunningMode::Service => Ok(service_log_dir()?.join("service_latest.log")), + RunningMode::Sidecar | RunningMode::NotRunning => { + Ok(sidecar_log_dir()?.join("sidecar_latest.log")) + } + } +} + pub fn path_to_str(path: &PathBuf) -> Result<&str> { let path_str = path .as_os_str() @@ -211,7 +236,11 @@ pub fn ensure_mihomo_safe_dir() -> Option { if home_config.exists() || fs::create_dir_all(&home_config).is_ok() { Some(home_config) } else { - log::error!(target: "app", "Failed to create safe directory: {home_config:?}"); + logging!( + error, + Type::File, + "Failed to create safe directory: {home_config:?}" + ); None } }) diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/src/utils/draft.rs index 044f6f1f..c34bbfb6 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/src/utils/draft.rs @@ -24,22 +24,21 @@ impl From for Draft { /// /// # Methods /// - `data_mut`: Returns a mutable reference to the committed data. -/// - `data_ref`: Returns an immutable reference to the committed data. /// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists. /// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data. /// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed. /// - `discard`: Discards the draft data and returns it if it existed. impl Draft> { + /// 正式数据视图 + pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box> { + RwLockReadGuard::map(self.inner.read(), |inner| &inner.0) + } + /// 可写正式数据 pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box> { RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0) } - /// 返回正式数据的只读视图(不包含草稿) - pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box> { - RwLockReadGuard::map(self.inner.read(), |inner| &inner.0) - } - /// 创建或获取草稿并返回可写引用 pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box> { let guard = self.inner.upgradable_read(); @@ -69,17 +68,21 @@ impl Draft> { } /// 提交草稿,返回旧正式数据 - pub fn apply(&self) -> Option> { - let mut inner = self.inner.write(); - inner - .1 - .take() - .map(|draft| std::mem::replace(&mut inner.0, draft)) + pub fn apply(&self) { + let guard = self.inner.upgradable_read(); + if guard.1.is_none() { + return; + } + + let mut guard = RwLockUpgradableReadGuard::upgrade(guard); + if let Some(draft) = guard.1.take() { + guard.0 = draft; + } } /// 丢弃草稿,返回被丢弃的草稿 - pub fn discard(&self) -> Option> { - self.inner.write().1.take() + pub fn discard(&self) { + self.inner.write().1.take(); } /// 异步修改正式数据,闭包直接获得 Box 所有权 @@ -152,8 +155,7 @@ fn test_draft_box() { } // 5. 提交草稿 - assert!(draft.apply().is_some()); // 第一次提交应有返回 - assert!(draft.apply().is_none()); // 第二次提交返回 None + draft.apply(); // 正式数据已更新 { @@ -170,8 +172,7 @@ fn test_draft_box() { assert_eq!(draft.draft_mut().enable_auto_launch, Some(true)); // 7. 丢弃草稿 - assert!(draft.discard().is_some()); // 第一次丢弃返回 Some - assert!(draft.discard().is_none()); // 再次丢弃返回 None + draft.discard(); // 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone assert_eq!(draft.draft_mut().enable_auto_launch, Some(false)); diff --git a/src-tauri/src/utils/i18n.rs b/src-tauri/src/utils/i18n.rs index 3ddcb5da..83f6140e 100644 --- a/src-tauri/src/utils/i18n.rs +++ b/src-tauri/src/utils/i18n.rs @@ -1,11 +1,18 @@ use crate::{config::Config, utils::dirs}; use once_cell::sync::Lazy; -use serde_json::Value; -use std::{fs, path::PathBuf, sync::RwLock}; +use smartstring::alias::String; +use std::{ + collections::HashMap, + fs, + path::PathBuf, + sync::{Arc, RwLock}, +}; use sys_locale; const DEFAULT_LANGUAGE: &str = "zh"; +type TranslationMap = (String, HashMap>); + fn get_locales_dir() -> Option { dirs::app_resources_dir() .map(|resource_path| resource_path.join("locales")) @@ -33,18 +40,33 @@ pub fn get_supported_languages() -> Vec { languages } -static TRANSLATIONS: Lazy> = Lazy::new(|| { +pub async fn current_language() -> String { + Config::verge() + .await + .latest_ref() + .language + .as_deref() + .map(String::from) + .unwrap_or_else(get_system_language) +} + +static TRANSLATIONS: Lazy> = Lazy::new(|| { let lang = get_system_language(); - let json = load_lang_file(&lang).unwrap_or_else(|| Value::Object(Default::default())); - RwLock::new((lang, json)) + let map = load_lang_file(&lang).unwrap_or_default(); + RwLock::new((lang, map)) }); -fn load_lang_file(lang: &str) -> Option { +fn load_lang_file(lang: &str) -> Option>> { let locales_dir = get_locales_dir()?; let file_path = locales_dir.join(format!("{lang}.json")); fs::read_to_string(file_path) .ok() - .and_then(|content| serde_json::from_str(&content).ok()) + .and_then(|content| serde_json::from_str::>(&content).ok()) + .map(|map| { + map.into_iter() + .map(|(k, v)| (k, Arc::from(v.as_str()))) + .collect() + }) } fn get_system_language() -> String { @@ -55,44 +77,38 @@ fn get_system_language() -> String { .unwrap_or_else(|| DEFAULT_LANGUAGE.into()) } -pub async fn t(key: &str) -> String { - let current_lang = Config::verge() - .await - .latest_ref() - .language - .as_deref() - .map(String::from) - .unwrap_or_else(get_system_language); +pub async fn t(key: &str) -> Arc { + let current_lang = current_language().await; { if let Ok(cache) = TRANSLATIONS.read() && cache.0 == current_lang - && let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) + && let Some(text) = cache.1.get(key) { - return text.into(); + return Arc::clone(text); } } - if let Some(new_json) = load_lang_file(¤t_lang) + if let Some(new_map) = load_lang_file(¤t_lang) && let Ok(mut cache) = TRANSLATIONS.write() { - *cache = (current_lang.clone(), new_json); + *cache = (current_lang.clone(), new_map); - if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) { - return text.into(); + if let Some(text) = cache.1.get(key) { + return Arc::clone(text); } } if current_lang != DEFAULT_LANGUAGE - && let Some(default_json) = load_lang_file(DEFAULT_LANGUAGE) + && let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE) && let Ok(mut cache) = TRANSLATIONS.write() { - *cache = (DEFAULT_LANGUAGE.into(), default_json); + *cache = (DEFAULT_LANGUAGE.into(), default_map); - if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) { - return text.into(); + if let Some(text) = cache.1.get(key) { + return Arc::clone(text); } } - key.into() + Arc::from(key) } diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 63b481d9..61d4288c 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -3,6 +3,7 @@ use crate::utils::logging::NoModuleFilter; use crate::{ config::*, + constants, core::handle, logging, process::AsyncHandler, @@ -304,7 +305,7 @@ async fn init_dns_config() -> Result<()> { // 检查DNS配置文件是否存在 let app_dir = dirs::app_home_dir()?; - let dns_path = app_dir.join("dns_config.yaml"); + let dns_path = app_dir.join(constants::files::DNS_CONFIG); if !dns_path.exists() { logging!(info, Type::Setup, "Creating default DNS config file"); @@ -429,26 +430,8 @@ pub async fn init_resources() -> Result<()> { let src_path = res_dir.join(file); let dest_path = app_dir.join(file); - let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move { - match fs::copy(&src, &dest).await { - Ok(_) => { - logging!(debug, Type::Setup, "resources copied '{}'", file); - } - Err(err) => { - logging!( - error, - Type::Setup, - "failed to copy resources '{}' to '{:?}', {}", - file, - dest, - err - ); - } - }; - }; - if src_path.exists() && !dest_path.exists() { - handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await; + handle_copy(&src_path, &dest_path, file).await; continue; } @@ -458,12 +441,12 @@ pub async fn init_resources() -> Result<()> { match (src_modified, dest_modified) { (Ok(src_modified), Ok(dest_modified)) => { if src_modified > dest_modified { - handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await; + handle_copy(&src_path, &dest_path, file).await; } } _ => { logging!(debug, Type::Setup, "failed to get modified '{}'", file); - handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await; + handle_copy(&src_path, &dest_path, file).await; } }; } @@ -563,3 +546,21 @@ pub async fn startup_script() -> Result<()> { Ok(()) } + +async fn handle_copy(src: &PathBuf, dest: &PathBuf, file: &str) { + match fs::copy(src, dest).await { + Ok(_) => { + logging!(debug, Type::Setup, "resources copied '{}'", file); + } + Err(err) => { + logging!( + error, + Type::Setup, + "failed to copy resources '{}' to '{:?}', {}", + file, + dest, + err + ); + } + }; +} diff --git a/src-tauri/src/utils/logging.rs b/src-tauri/src/utils/logging.rs index f4c6d740..9f73c634 100644 --- a/src-tauri/src/utils/logging.rs +++ b/src-tauri/src/utils/logging.rs @@ -11,7 +11,7 @@ use tokio::sync::{Mutex, MutexGuard}; pub type SharedWriter = Arc>; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub enum Type { Cmd, Core, @@ -29,7 +29,6 @@ pub enum Type { Lightweight, Network, ProxyMode, - // Cache, Validate, ClashVergeRev, } @@ -53,7 +52,6 @@ impl fmt::Display for Type { Type::Lightweight => write!(f, "[Lightweight]"), Type::Network => write!(f, "[Network]"), Type::ProxyMode => write!(f, "[ProxMode]"), - // Type::Cache => write!(f, "[Cache]"), Type::Validate => write!(f, "[Validate]"), Type::ClashVergeRev => write!(f, "[ClashVergeRev]"), } @@ -82,15 +80,6 @@ macro_rules! log_err { }; } -#[macro_export] -macro_rules! trace_err { - ($result: expr, $err_str: expr) => { - if let Err(err) = $result { - log::trace!(target: "app", "{}, err {}", $err_str, err); - } - } -} - /// wrap the anyhow error /// transform the error to String #[macro_export] diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index cc765e65..65ef63a2 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -1,6 +1,7 @@ use crate::config::Config; use anyhow::Result; use base64::{Engine as _, engine::general_purpose}; +use isahc::config::DnsCache; use isahc::prelude::*; use isahc::{HttpClient, config::SslOption}; use isahc::{ @@ -143,6 +144,12 @@ impl NetworkManager { builder = builder.redirect_policy(RedirectPolicy::Follow); + // 禁用缓存,不关心连接复用 + builder = builder.connection_cache_size(0); + + // 禁用 DNS 缓存,避免因 DNS 变化导致的问题 + builder = builder.dns_cache(DnsCache::Disable); + Ok(builder.build()?) } } diff --git a/src-tauri/src/utils/notification.rs b/src-tauri/src/utils/notification.rs index 0b0b7199..f737560e 100644 --- a/src-tauri/src/utils/notification.rs +++ b/src-tauri/src/utils/notification.rs @@ -1,6 +1,5 @@ -use crate::utils::i18n::t; +use crate::{core::handle, utils::i18n::t}; -use tauri::AppHandle; use tauri_plugin_notification::NotificationExt; pub enum NotificationEvent<'a> { @@ -16,8 +15,10 @@ pub enum NotificationEvent<'a> { AppHidden, } -fn notify(app: &AppHandle, title: &str, body: &str) { - app.notification() +fn notify(title: &str, body: &str) { + let app_handle = handle::Handle::app_handle(); + app_handle + .notification() .builder() .title(title) .body(body) @@ -25,49 +26,44 @@ fn notify(app: &AppHandle, title: &str, body: &str) { .ok(); } -pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) { +pub async fn notify_event<'a>(event: NotificationEvent<'a>) { match event { NotificationEvent::DashboardToggled => { notify( - &app, &t("DashboardToggledTitle").await, &t("DashboardToggledBody").await, ); } NotificationEvent::ClashModeChanged { mode } => { notify( - &app, &t("ClashModeChangedTitle").await, &t_with_args("ClashModeChangedBody", mode).await, ); } NotificationEvent::SystemProxyToggled => { notify( - &app, &t("SystemProxyToggledTitle").await, &t("SystemProxyToggledBody").await, ); } NotificationEvent::TunModeToggled => { notify( - &app, &t("TunModeToggledTitle").await, &t("TunModeToggledBody").await, ); } NotificationEvent::LightweightModeEntered => { notify( - &app, &t("LightweightModeEnteredTitle").await, &t("LightweightModeEnteredBody").await, ); } NotificationEvent::AppQuit => { - notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await); + notify(&t("AppQuitTitle").await, &t("AppQuitBody").await); } #[cfg(target_os = "macos")] NotificationEvent::AppHidden => { - notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await); + notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await); } } } diff --git a/src-tauri/src/utils/resolve/dns.rs b/src-tauri/src/utils/resolve/dns.rs index 4b710bdc..e225f0c8 100644 --- a/src-tauri/src/utils/resolve/dns.rs +++ b/src-tauri/src/utils/resolve/dns.rs @@ -1,20 +1,27 @@ #[cfg(target_os = "macos")] +use crate::{logging, utils::logging::Type}; pub async fn set_public_dns(dns_server: String) { - use crate::{core::handle, utils::dirs}; + use crate::utils::logging::Type; + use crate::{core::handle, logging, utils::dirs}; use tauri_plugin_shell::ShellExt; let app_handle = handle::Handle::app_handle(); - log::info!(target: "app", "try to set system dns"); + logging!(info, Type::Config, "try to set system dns"); let resource_dir = match dirs::app_resources_dir() { Ok(dir) => dir, Err(e) => { - log::error!(target: "app", "Failed to get resource directory: {}", e); + logging!( + error, + Type::Config, + "Failed to get resource directory: {}", + e + ); return; } }; let script = resource_dir.join("set_dns.sh"); if !script.exists() { - log::error!(target: "app", "set_dns.sh not found"); + logging!(error, Type::Config, "set_dns.sh not found"); return; } let script = script.to_string_lossy().into_owned(); @@ -28,14 +35,14 @@ pub async fn set_public_dns(dns_server: String) { { Ok(status) => { if status.success() { - log::info!(target: "app", "set system dns successfully"); + logging!(info, Type::Config, "set system dns successfully"); } else { let code = status.code().unwrap_or(-1); - log::error!(target: "app", "set system dns failed: {code}"); + logging!(error, Type::Config, "set system dns failed: {code}"); } } Err(err) => { - log::error!(target: "app", "set system dns failed: {err}"); + logging!(error, Type::Config, "set system dns failed: {err}"); } } } @@ -45,17 +52,22 @@ pub async fn restore_public_dns() { use crate::{core::handle, utils::dirs}; use tauri_plugin_shell::ShellExt; let app_handle = handle::Handle::app_handle(); - log::info!(target: "app", "try to unset system dns"); + logging!(info, Type::Config, "try to unset system dns"); let resource_dir = match dirs::app_resources_dir() { Ok(dir) => dir, Err(e) => { - log::error!(target: "app", "Failed to get resource directory: {}", e); + logging!( + error, + Type::Config, + "Failed to get resource directory: {}", + e + ); return; } }; let script = resource_dir.join("unset_dns.sh"); if !script.exists() { - log::error!(target: "app", "unset_dns.sh not found"); + logging!(error, Type::Config, "unset_dns.sh not found"); return; } let script = script.to_string_lossy().into_owned(); @@ -69,14 +81,14 @@ pub async fn restore_public_dns() { { Ok(status) => { if status.success() { - log::info!(target: "app", "unset system dns successfully"); + logging!(info, Type::Config, "unset system dns successfully"); } else { let code = status.code().unwrap_or(-1); - log::error!(target: "app", "unset system dns failed: {code}"); + logging!(error, Type::Config, "unset system dns failed: {code}"); } } Err(err) => { - log::error!(target: "app", "unset system dns failed: {err}"); + logging!(error, Type::Config, "unset system dns failed: {err}"); } } } diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 329c3a86..cb0989b1 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use smartstring::alias::String; use crate::{ config::Config, @@ -11,10 +10,7 @@ use crate::{ tray::Tray, }, logging, logging_error, - module::{ - lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight}, - signal, - }, + module::{lightweight::auto_lightweight_boot, signal}, process::AsyncHandler, utils::{init, logging::Type, server, window_manager::WindowManager}, }; @@ -71,8 +67,7 @@ pub fn resolve_setup_async() { tray_init, init_timer(), init_hotkey(), - init_auto_lightweight_mode(), - init_once_auto_lightweight(), + init_auto_lightweight_boot(), ); }); } @@ -103,7 +98,7 @@ pub(super) async fn resolve_setup_logger() { logging_error!(Type::Setup, init::init_logger().await); } -pub async fn resolve_scheme(param: String) -> Result<()> { +pub async fn resolve_scheme(param: &str) -> Result<()> { logging_error!(Type::Setup, scheme::resolve_scheme(param).await); Ok(()) } @@ -128,12 +123,8 @@ pub(super) async fn init_hotkey() { logging_error!(Type::Setup, Hotkey::global().init().await); } -pub(super) async fn init_once_auto_lightweight() { - run_once_auto_lightweight().await; -} - -pub(super) async fn init_auto_lightweight_mode() { - logging_error!(Type::Setup, auto_lightweight_mode_init().await); +pub(super) async fn init_auto_lightweight_boot() { + logging_error!(Type::Setup, auto_lightweight_boot().await); } pub(super) fn init_signal() { diff --git a/src-tauri/src/utils/resolve/scheme.rs b/src-tauri/src/utils/resolve/scheme.rs index 4c7ace90..04659ab3 100644 --- a/src-tauri/src/utils/resolve/scheme.rs +++ b/src-tauri/src/utils/resolve/scheme.rs @@ -3,17 +3,22 @@ use percent_encoding::percent_decode_str; use smartstring::alias::String; use tauri::Url; -use crate::{config::PrfItem, core::handle, logging, logging_error, utils::logging::Type}; +use crate::{ + config::{PrfItem, profiles}, + core::handle, + logging, logging_error, + utils::logging::Type, +}; -pub(super) async fn resolve_scheme(param: String) -> Result<()> { - log::info!(target:"app", "received deep link: {param}"); +pub(super) async fn resolve_scheme(param: &str) -> Result<()> { + logging!(info, Type::Config, "received deep link: {param}"); let param_str = if param.starts_with("[") && param.len() > 4 { param .get(2..param.len() - 2) .ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))? } else { - param.as_str() + param }; // 解析 URL @@ -25,10 +30,11 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> { }; if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { - let name = link_parsed + let name_owned: Option = link_parsed .query_pairs() .find(|(key, _)| key == "name") - .map(|(_, value)| value.into()); + .map(|(_, value)| value.into_owned().into()); + let name = name_owned.as_ref(); let url_param = if let Some(query) = link_parsed.query() { let prefix = "url="; @@ -43,10 +49,10 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> { }; match url_param { - Some(url) => { - log::info!(target:"app", "decoded subscription url: {url}"); + Some(ref url) => { + logging!(info, Type::Config, "decoded subscription url: {url}"); match PrfItem::from_url(url.as_ref(), name, None, None).await { - Ok(item) => { + Ok(mut item) => { let uid = match item.uid.clone() { Some(uid) => uid, None => { @@ -58,7 +64,7 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> { return Ok(()); } }; - let result = crate::config::profiles::profiles_append_item_safe(item).await; + let result = profiles::profiles_append_item_safe(&mut item).await; logging_error!( Type::Config, "failed to import subscription url: {:?}", diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs index f415f506..387145ec 100644 --- a/src-tauri/src/utils/resolve/window.rs +++ b/src-tauri/src/utils/resolve/window.rs @@ -21,16 +21,14 @@ const MINIMAL_HEIGHT: f64 = 520.0; pub async fn build_new_window() -> Result { let app_handle = handle::Handle::app_handle(); - let start_page = Config::verge() - .await - .latest_ref() - .start_page - .clone() - .unwrap_or_else(|| "/".into()); + let config = Config::verge().await; + let latest = config.latest_ref(); + let start_page = latest.start_page.as_deref().unwrap_or("/"); + match tauri::WebviewWindowBuilder::new( app_handle, "main", /* the unique window label */ - tauri::WebviewUrl::App(start_page.as_str().into()), + tauri::WebviewUrl::App(start_page.into()), ) .title("Clash Verge") .center() diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 991e29de..c5062a8c 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -51,7 +51,11 @@ pub async fn check_singleton() -> Result<()> { .send() .await?; } - log::error!("failed to setup singleton listen server"); + logging!( + error, + Type::Window, + "failed to setup singleton listen server" + ); bail!("app exists"); } Ok(()) @@ -107,9 +111,8 @@ pub fn embed_server() { let scheme = warp::path!("commands" / "scheme") .and(warp::query::()) .map(|query: QueryParam| { - let param = query.param.clone(); tokio::task::spawn_local(async move { - logging_error!(Type::Setup, resolve::resolve_scheme(param).await); + logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); }); warp::reply::with_status::( "ok".to_string(), @@ -130,7 +133,7 @@ pub fn embed_server() { } pub fn shutdown_embedded_server() { - log::info!("shutting down embedded server"); + logging!(info, Type::Window, "shutting down embedded server"); if let Some(sender) = SHUTDOWN_SENDER.get() && let Some(sender) = sender.lock().take() { diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index 2e9c3054..2d5b7a1b 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -58,7 +58,11 @@ fn get_window_operation_debounce() -> &'static Mutex { fn should_handle_window_operation() -> bool { if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { - log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用"); + logging!( + warn, + Type::Window, + "Warning: [防抖] 窗口操作已在进行中,跳过重复调用" + ); return false; } @@ -67,17 +71,27 @@ fn should_handle_window_operation() -> bool { let now = Instant::now(); let elapsed = now.duration_since(*last_operation); - log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", - elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + logging!( + debug, + Type::Window, + "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", + elapsed.as_millis(), + WINDOW_OPERATION_DEBOUNCE_MS + ); if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { *last_operation = now; WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); - log::info!(target: "app", "[防抖] 窗口操作被允许执行"); + logging!(info, Type::Window, "[防抖] 窗口操作被允许执行"); true } else { - log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", - elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + logging!( + warn, + Type::Window, + "Warning: [防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", + elapsed.as_millis(), + WINDOW_OPERATION_DEBOUNCE_MS + ); false } } @@ -359,7 +373,6 @@ impl WindowManager { } return WindowOperationResult::Destroyed; } - logging!(warn, Type::Window, "窗口摧毁失败"); WindowOperationResult::Failed } diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index ceea82d7..c5061938 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -506,7 +506,7 @@ export const CurrentProxyCard = () => { // 导航到代理页面 const goToProxies = useCallback(() => { - navigate("/"); + navigate("/proxies"); }, [navigate]); // 获取要显示的代理节点 diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts index 145bc5bd..7682b0a9 100644 --- a/src/components/layout/use-custom-theme.ts +++ b/src/components/layout/use-custom-theme.ts @@ -53,10 +53,13 @@ export const useCustomTheme = () => { return; } - if ( + const preferBrowserMatchMedia = typeof window !== "undefined" && - typeof window.matchMedia === "function" - ) { + typeof window.matchMedia === "function" && + // Skip Tauri flow when running purely in browser. + !("__TAURI__" in window); + + if (preferBrowserMatchMedia) { return; } diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index c9ffeb23..48aa6385 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -341,7 +341,8 @@ export const ProfileItem = (props: Props) => { try { // 调用后端更新(后端会自动处理回退逻辑) - await updateProfile(itemData.uid, option); + const payload = Object.keys(option).length > 0 ? option : undefined; + await updateProfile(itemData.uid, payload); // 更新成功,刷新列表 mutate("getProfiles"); diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 0c9d2c5a..d300822a 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -307,7 +307,7 @@ export const LayoutViewer = forwardRef((_, ref) => { )} */} - {OS === "macos" && ( + {/* {OS === "macos" && ( ((_, ref) => { - )} + )} */} (null); const tunRef = useRef(null); @@ -148,9 +143,7 @@ const ProxyControlSwitches = ({ const onInstallService = useLockFn(async () => { try { await installServiceAndRestartCore(); - await mutateRunningMode(); - await mutateServiceOk(); - await mutateTunModeAvailable(); + await mutateSystemState(); } catch (err) { showNotice("error", (err as Error).message || String(err)); } @@ -158,11 +151,11 @@ const ProxyControlSwitches = ({ const onUninstallService = useLockFn(async () => { try { - await handleTunToggle(false); + if (verge?.enable_tun_mode) { + await handleTunToggle(false); + } await uninstallServiceAndRestartCore(); - await mutateRunningMode(); - await mutateServiceOk(); - await mutateTunModeAvailable(); + await mutateSystemState(); } catch (err) { showNotice("error", (err as Error).message || String(err)); } @@ -198,22 +191,22 @@ const ProxyControlSwitches = ({ extraIcons={ <> {!isTunModeAvailable && ( - + <> + + + )} - {!isTunModeAvailable && ( - - )} - {isServiceMode && ( + {isServiceOk && ( { const patchCurrent = async (value: Partial) => { if (profiles?.current) { await patchProfile(profiles.current, value); - mutateProfiles(); + if (!value.selected) { + mutateProfiles(); + } } }; diff --git a/src/hooks/use-system-state.ts b/src/hooks/use-system-state.ts index 1a4f791c..37425270 100644 --- a/src/hooks/use-system-state.ts +++ b/src/hooks/use-system-state.ts @@ -1,73 +1,99 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import useSWR from "swr"; import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +import { useVerge } from "./use-verge"; + +export interface SystemState { + runningMode: "Sidecar" | "Service"; + isAdminMode: boolean; + isServiceOk: boolean; +} + +const defaultSystemState = { + runningMode: "Sidecar", + isAdminMode: false, + isServiceOk: false, +} as SystemState; + +let disablingTunMode = false; /** * 自定义 hook 用于获取系统运行状态 * 包括运行模式、管理员状态、系统服务是否可用 */ export function useSystemState() { - // 获取运行模式 - const { - data: runningMode = "Sidecar", - mutate: mutateRunningMode, - isLoading: runningModeLoading, - } = useSWR("getRunningMode", getRunningMode, { - suspense: false, - revalidateOnFocus: false, - }); - const isSidecarMode = runningMode === "Sidecar"; - const isServiceMode = runningMode === "Service"; + const { t } = useTranslation(); + const { verge, patchVerge } = useVerge(); - // 获取管理员状态 - const { data: isAdminMode = false, isLoading: isAdminLoading } = useSWR( - "isAdmin", - isAdmin, + const { + data: systemState, + mutate: mutateSystemState, + isLoading, + } = useSWR( + "getSystemState", + async () => { + const [runningMode, isAdminMode, isServiceOk] = await Promise.all([ + getRunningMode(), + isAdmin(), + isServiceAvailable(), + ]); + return { runningMode, isAdminMode, isServiceOk } as SystemState; + }, { - suspense: false, - revalidateOnFocus: false, + suspense: true, + refreshInterval: 30000, + fallback: defaultSystemState, }, ); - const { - data: isServiceOk = false, - mutate: mutateServiceOk, - isLoading: isServiceLoading, - } = useSWR(isServiceMode ? "isServiceAvailable" : null, isServiceAvailable, { - suspense: false, - revalidateOnFocus: false, - onSuccess: (data) => { - console.log("[useSystemState] 服务状态更新:", data); - }, - onError: (error) => { - console.error("[useSystemState] 服务状态检查失败:", error); - }, - // isPaused: () => !isServiceMode, // 仅在非 Service 模式下暂停请求 - }); + const isSidecarMode = systemState.runningMode === "Sidecar"; + const isServiceMode = systemState.runningMode === "Service"; + const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk; - const isLoading = - runningModeLoading || isAdminLoading || (isServiceMode && isServiceLoading); + const enable_tun_mode = verge?.enable_tun_mode; + useEffect(() => { + if (enable_tun_mode === undefined) return; - const { data: isTunModeAvailable = false, mutate: mutateTunModeAvailable } = - useSWR( - ["isTunModeAvailable", isAdminMode, isServiceOk], - () => isAdminMode || isServiceOk, - { - suspense: false, - revalidateOnFocus: false, - }, - ); + if ( + !disablingTunMode && + enable_tun_mode && + !isTunModeAvailable && + !isLoading + ) { + disablingTunMode = true; + patchVerge({ enable_tun_mode: false }) + .then(() => { + showNotice( + "info", + t("TUN Mode automatically disabled due to service unavailable"), + ); + }) + .catch((err) => { + console.error("[useVerge] 自动关闭TUN模式失败:", err); + showNotice("error", t("Failed to disable TUN Mode automatically")); + }) + .finally(() => { + const tid = setTimeout(() => { + // 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式 + disablingTunMode = false; + clearTimeout(tid); + }, 1000); + }); + } + }, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading, t]); return { - runningMode, - isAdminMode, + runningMode: systemState.runningMode, + isAdminMode: systemState.isAdminMode, + isServiceOk: systemState.isServiceOk, isSidecarMode, isServiceMode, - isServiceOk, isTunModeAvailable, - mutateRunningMode, - mutateServiceOk, - mutateTunModeAvailable, + mutateSystemState, isLoading, }; } diff --git a/src/hooks/use-verge.ts b/src/hooks/use-verge.ts index 1e7d7afc..f6415ff8 100644 --- a/src/hooks/use-verge.ts +++ b/src/hooks/use-verge.ts @@ -1,16 +1,8 @@ -import { useCallback, useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; import useSWR from "swr"; -import { useSystemState } from "@/hooks/use-system-state"; import { getVergeConfig, patchVergeConfig } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; export const useVerge = () => { - const { t } = useTranslation(); - const { isTunModeAvailable, isServiceMode, isLoading } = useSystemState(); - const disablingRef = useRef(false); - const { data: verge, mutate: mutateVerge } = useSWR( "getVergeConfig", async () => { @@ -24,53 +16,6 @@ export const useVerge = () => { mutateVerge(); }; - const { enable_tun_mode } = verge ?? {}; - - const mutateVergeRef = useRef(mutateVerge); - const tRef = useRef(t); - const enableTunRef = useRef(enable_tun_mode); - const isLoadingRef = useRef(isLoading); - const isServiceModeRef = useRef(isServiceMode); - - mutateVergeRef.current = mutateVerge; - tRef.current = t; - enableTunRef.current = enable_tun_mode; - isLoadingRef.current = isLoading; - isServiceModeRef.current = isServiceMode; - - const doDisable = useCallback(async () => { - try { - if (isServiceModeRef.current === true) return; - await patchVergeConfig({ enable_tun_mode: false }); - await mutateVergeRef.current?.(); - showNotice( - "info", - tRef.current( - "TUN Mode automatically disabled due to service unavailable", - ), - ); - } catch (err) { - console.error("[useVerge] 自动关闭TUN模式失败:", err); - showNotice( - "error", - tRef.current("Failed to disable TUN Mode automatically"), - ); - } finally { - disablingRef.current = false; - } - }, []); - - useEffect(() => { - if (isTunModeAvailable === true) return; - if (isLoadingRef.current === true) return; - if (enableTunRef.current !== true) return; - if (isServiceModeRef.current === true) return; - if (disablingRef.current) return; - - disablingRef.current = true; - void doDisable(); - }, [isTunModeAvailable, doDisable]); - return { verge, mutateVerge, diff --git a/src/hooks/useServiceInstaller.ts b/src/hooks/useServiceInstaller.ts index b074c678..2f216876 100644 --- a/src/hooks/useServiceInstaller.ts +++ b/src/hooks/useServiceInstaller.ts @@ -25,7 +25,7 @@ const executeWithErrorHandling = async ( }; export const useServiceInstaller = () => { - const { mutateRunningMode, mutateServiceOk } = useSystemState(); + const { mutateSystemState } = useSystemState(); const installServiceAndRestartCore = useCallback(async () => { await executeWithErrorHandling( @@ -34,9 +34,13 @@ export const useServiceInstaller = () => { "Service Installed Successfully", ); - await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); - await mutateRunningMode(); - await mutateServiceOk(); - }, [mutateRunningMode, mutateServiceOk]); + await executeWithErrorHandling( + () => restartCore(), + "Restarting Core...", + "Clash Core Restarted", + ); + + await mutateSystemState(); + }, [mutateSystemState]); return { installServiceAndRestartCore }; }; diff --git a/src/hooks/useServiceUninstaller.ts b/src/hooks/useServiceUninstaller.ts index bb2c450d..fbcd76a5 100644 --- a/src/hooks/useServiceUninstaller.ts +++ b/src/hooks/useServiceUninstaller.ts @@ -25,21 +25,26 @@ const executeWithErrorHandling = async ( }; export const useServiceUninstaller = () => { - const { mutateRunningMode, mutateServiceOk } = useSystemState(); + const { mutateSystemState } = useSystemState(); const uninstallServiceAndRestartCore = useCallback(async () => { - await executeWithErrorHandling(() => stopCore(), "Stopping Core..."); - - await executeWithErrorHandling( - () => uninstallService(), - "Uninstalling Service...", - "Service Uninstalled Successfully", - ); - - await executeWithErrorHandling(() => restartCore(), "Restarting Core..."); - await mutateRunningMode(); - await mutateServiceOk(); - }, [mutateRunningMode, mutateServiceOk]); + try { + await executeWithErrorHandling(() => stopCore(), "Stopping Core..."); + await executeWithErrorHandling( + () => uninstallService(), + "Uninstalling Service...", + "Service Uninstalled Successfully", + ); + } catch (ignore) { + } finally { + await executeWithErrorHandling( + () => restartCore(), + "Restarting Core...", + "Clash Core Restarted", + ); + await mutateSystemState(); + } + }, [mutateSystemState]); return { uninstallServiceAndRestartCore }; }; diff --git a/src/locales/ar.json b/src/locales/ar.json index 41138721..ded40404 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -21,7 +21,6 @@ "Label-Connections": "الاتصالات", "Label-Rules": "القواعد", "Label-Logs": "السجلات", - "Label-Test": "اختبار", "Label-Settings": "الإعدادات", "Proxies": "الوكلاء", "Proxy Groups": "مجموعات الوكلاء", @@ -166,7 +165,6 @@ "Table View": "عرض الجدول", "List View": "عرض القائمة", "Close All": "إغلاق الكل", - "Default": "افتراضي", "Download Speed": "سرعة التنزيل", "Upload Speed": "سرعة الرفع", "Host": "المضيف", @@ -197,6 +195,8 @@ "Settings": "الإعدادات", "System Setting": "إعدادات النظام", "Tun Mode": "وضع TUN", + "TUN requires Service Mode": "يتطلب وضع TUN خدمة", + "Install Service": "تثبيت الخدمة ", "Reset to Default": "إعادة تعيين إلى الافتراضي", "Tun Mode Info": "وضع TUN (بطاقة شبكة افتراضية): يلتقط كل حركة المرور في النظام. عند تمكينه، لا حاجة لتفعيل وكيل النظام.", "Stack": "مكدس TUN", @@ -279,7 +279,8 @@ "Open UWP tool": "فتح أداة UWP", "Open UWP tool Info": "منذ نظام ويندوز 8، يتم تقييد تطبيقات UWP من الوصول المباشر إلى المضيف المحلي. هذه الأداة تتيح تجاوز هذا التقييد", "Update GeoData": "تحديث البيانات الجغرافية", - "Verge Setting": "إعدادات Verge", + "Verge Basic Setting": "الإعدادات الأساسية Verge", + "Verge Advanced Setting": "الإعدادات الأساسية Verge", "Language": "اللغة", "Theme Mode": "وضع السمة", "theme.light": "سمة فاتحة", @@ -365,6 +366,7 @@ "Profile Reactivated": "تم إعادة تنشيط الملف الشخصي", "Only YAML Files Supported": "لا يتم دعم سوى ملفات YAML", "Settings Applied": "تم تطبيق الإعدادات", + "Installing Service...": "جاري تثبيت الخدمة...", "Service Installed Successfully": "تم تثبيت الخدمة بنجاح", "Service Uninstalled Successfully": "تم إلغاء تثبيت الخدمة بنجاح", "Proxy Daemon Duration Cannot be Less than 1 Second": "لا يمكن أن تقل مدة خادم الوكيل عن ثانية واحدة", @@ -407,19 +409,12 @@ "Help": "مساعدة", "About": "حول", "Theme": "السمة", - "TUN Mode": "وضع TUN", "Main Window": "النافذة الرئيسية", "Group Icon": "أيقونة المجموعة", "Menu Icon": "أيقونة القائمة", - "System Proxy Bypass": "تخطي وكيل النظام", "PAC File": "ملف PAC", "Web UI": "واجهة الويب", "Hotkeys": "اختصارات لوحة المفاتيح", - "Auto Close Connection": "إغلاق الاتصال تلقائيًا", - "Enable Built-in Enhanced": "تفعيل التحسين المدمج", - "Proxy Layout Column": "عمود عرض الوكيل", - "Test List": "قائمة الاختبارات", - "Enable Random Port": "تفعيل المنفذ العشوائي", "Verge Mixed Port": "منفذ Verge المختلط", "Verge Socks Port": "منفذ Verge SOCKS", "Verge Redir Port": "منفذ إعادة التوجيه لـ Verge", @@ -429,15 +424,16 @@ "WebDAV URL": "رابط WebDAV", "WebDAV Username": "اسم المستخدم لـ WebDAV", "WebDAV Password": "كلمة مرور WebDAV", + "Dashboard": "لوحة التحكم", + "Restart App": "إعادة تشغيل التطبيق", + "Restart Clash Core": "إعادة تشغيل نواة Clash", + "TUN Mode": "وضع TUN", "Copy Env": "نسخ البيئة", "Conf Dir": "مجلد الإعدادات", "Core Dir": "مجلد النواة", "Logs Dir": "مجلد السجلات", "Open Dir": "فتح المجلد", - "Restart Clash Core": "إعادة تشغيل نواة Clash", - "Restart App": "إعادة تشغيل التطبيق", "More": "المزيد", - "Dashboard": "لوحة التحكم", "Rule Mode": "وضع القواعد", "Global Mode": "الوضع العالمي", "Direct Mode": "الوضع المباشر", @@ -454,10 +450,14 @@ "Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات", "Core Changed Successfully": "تم تغيير النواة بنجاح", "Failed to Change Core": "فشل تغيير النواة", - "Verge Basic Setting": "الإعدادات الأساسية Verge", - "Verge Advanced Setting": "الإعدادات الأساسية Verge", - "TUN requires Service Mode": "يتطلب وضع TUN خدمة", - "Install Service": "تثبيت الخدمة ", - "Installing Service...": "جاري تثبيت الخدمة...", - "Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام" + "Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام", + "Auto Close Connection": "إغلاق الاتصال تلقائيًا", + "Default": "افتراضي", + "Enable Built-in Enhanced": "تفعيل التحسين المدمج", + "Enable Random Port": "تفعيل المنفذ العشوائي", + "Label-Test": "اختبار", + "Proxy Layout Column": "عمود عرض الوكيل", + "System Proxy Bypass": "تخطي وكيل النظام", + "Test List": "قائمة الاختبارات", + "Verge Setting": "إعدادات Verge" } diff --git a/src/locales/de.json b/src/locales/de.json index e6e6e03a..2c989cfc 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -195,10 +195,11 @@ "Settings": "Einstellungen", "System Setting": "Systemeinstellungen", "Tun Mode": "Virtual Network Interface-Modus", - "TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus", "Install Service": "Service installieren", + "Uninstall Service": "Dienst deinstallieren", "Reset to Default": "Auf Standardwerte zurücksetzen", "Tun Mode Info": "Der TUN-Modus (Virtual Network Interface) übernimmt den gesamten Systemverkehr. Wenn dieser Modus aktiviert ist, muss der Systemproxy nicht geöffnet werden.", + "TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus", "System Proxy Enabled": "Der Systemproxy ist aktiviert. Ihre Anwendungen werden über den Proxy auf das Netzwerk zugreifen.", "System Proxy Disabled": "Der Systemproxy ist deaktiviert. Es wird empfohlen, diesen Eintrag für die meisten Benutzer zu aktivieren.", "TUN Mode Enabled": "Der TUN-Modus ist aktiviert. Die Anwendungen werden über die virtuelle Netzwerkschnittstelle auf das Netzwerk zugreifen.", @@ -254,6 +255,7 @@ "Unified Delay Info": "Wenn die einheitliche Latenz aktiviert ist, werden zwei Latenztests durchgeführt, um die Latenzunterschiede zwischen verschiedenen Knotentypen aufgrund von Verbindungsaufbau und anderen Faktoren zu eliminieren.", "Log Level": "Protokolliergrad", "Log Level Info": "Dies wirkt sich nur auf die Kernprotokolldateien im Verzeichnis Service im Protokollverzeichnis aus.", + "Port Config": "Port-Konfiguration", "Random Port": "Zufälliger Port", "Mixed Port": "Mischter Proxy-Port", "Socks Port": "SOCKS-Proxy-Port", @@ -367,17 +369,16 @@ "Stopping Core...": "Kern wird gestoppt...", "Restarting Core...": "Kern wird neu gestartet...", "Installing Service...": "Service wird installiert...", - "Uninstall Service": "Dienst deinstallieren", - "Service Installed Successfully": "Service erfolgreich installiert", - "Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet", - "Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar", - "Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.", - "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.", "Uninstalling Service...": "Service wird deinstalliert...", - "Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...", + "Service Installed Successfully": "Service erfolgreich installiert", "Service Uninstalled Successfully": "Service erfolgreich deinstalliert", "Proxy Daemon Duration Cannot be Less than 1 Second": "Das Intervall des Proxy-Daemons darf nicht weniger als 1 Sekunde betragen.", "Invalid Bypass Format": "Ungültiges Format für die Proxy-Umgehung", + "Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...", + "Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.", + "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.", + "Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet", + "Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar", "Core Version Updated": "Kernversion wurde aktualisiert", "Clash Core Restarted": "Clash-Kern wurde neu gestartet", "GeoData Updated": "Geo-Daten wurden aktualisiert", @@ -523,7 +524,6 @@ "Unknown": "Unbekannt", "Auto update disabled": "Automatische Aktualisierung deaktiviert", "Update subscription successfully": "Abonnement erfolgreich aktualisiert", - "Update failed, retrying with Clash proxy...": "Abonnement-Aktualisierung fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...", "Update with Clash proxy successfully": "Aktualisierung mit Clash-Proxy erfolgreich", "Update failed even with Clash proxy": "Aktualisierung auch mit Clash-Proxy fehlgeschlagen", "Profile creation failed, retrying with Clash proxy...": "Erstellung des Abonnements fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...", @@ -555,10 +555,9 @@ "Disallowed ISP": "Nicht zugelassener Internetdienstanbieter", "Originals Only": "Nur Original", "Unsupported Country/Region": "Nicht unterstütztes Land/Region", + "Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert", "Controller address copied to clipboard": "API-Port in die Zwischenablage kopiert", "Secret copied to clipboard": "API-Schlüssel in die Zwischenablage kopiert", "Copy to clipboard": "Klicken Sie hier, um zu kopieren", - "Port Config": "Port-Konfiguration", - "Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert", "Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren" } diff --git a/src/locales/en.json b/src/locales/en.json index 28befbd2..2716d316 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -628,7 +628,6 @@ "Unknown": "Unknown", "Auto update disabled": "Auto update disabled", "Update subscription successfully": "Update subscription successfully", - "Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...", "Update with Clash proxy successfully": "Update with Clash proxy successfully", "Update failed even with Clash proxy": "Update failed even with Clash proxy", "Profile creation failed, retrying with Clash proxy...": "Profile creation failed, retrying with Clash proxy...", @@ -713,5 +712,7 @@ "Allow Auto Update": "Allow Auto Update", "Menu reorder mode": "Menu reorder mode", "Unlock menu order": "Unlock menu order", - "Lock menu order": "Lock menu order" + "Lock menu order": "Lock menu order", + "Open App Log": "Open App Log", + "Open Core Log": "Open Core Log" } diff --git a/src/locales/es.json b/src/locales/es.json index 456a4da4..ede76225 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -195,10 +195,11 @@ "Settings": "Ajustes", "System Setting": "Ajustes del sistema", "Tun Mode": "Modo de interfaz virtual (TUN)", - "TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador", "Install Service": "Instalar servicio", + "Uninstall Service": "Desinstalar servicio", "Reset to Default": "Restablecer a los valores predeterminados", "Tun Mode Info": "El modo TUN (interfaz virtual) gestiona todo el tráfico del sistema. No es necesario habilitar el proxy del sistema cuando está activado.", + "TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador", "System Proxy Enabled": "El proxy del sistema está habilitado. Sus aplicaciones accederán a Internet a través del proxy.", "System Proxy Disabled": "El proxy del sistema está deshabilitado. Se recomienda a la mayoría de los usuarios habilitar esta opción.", "TUN Mode Enabled": "El modo TUN está habilitado. Las aplicaciones accederán a Internet a través de la interfaz virtual.", @@ -254,6 +255,7 @@ "Unified Delay Info": "Al habilitar la latencia unificada, se realizarán dos pruebas de latencia para eliminar las diferencias de latencia entre diferentes tipos de nodos causadas por el handshake de conexión, etc.", "Log Level": "Nivel de registro", "Log Level Info": "Solo se aplica al archivo de registro del núcleo en la carpeta Service del directorio de registros.", + "Port Config": "Configuración de puerto", "Random Port": "Puerto aleatorio", "Mixed Port": "Puerto de proxy mixto", "Socks Port": "Puerto de proxy SOCKS", @@ -367,17 +369,16 @@ "Stopping Core...": "Deteniendo núcleo...", "Restarting Core...": "Reiniciando núcleo...", "Installing Service...": "Instalando servicio...", - "Uninstall Service": "Desinstalar servicio", - "Service Installed Successfully": "Servicio instalado con éxito", - "Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado", - "Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.", - "Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.", - "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.", "Uninstalling Service...": "Desinstalando servicio...", - "Waiting for service to be ready...": "Esperando a que el servicio esté listo...", + "Service Installed Successfully": "Servicio instalado con éxito", "Service Uninstalled Successfully": "Servicio desinstalado con éxito", "Proxy Daemon Duration Cannot be Less than 1 Second": "El intervalo de tiempo del daemon de proxy no puede ser menor de 1 segundo", "Invalid Bypass Format": "Formato de omisión de proxy no válido", + "Waiting for service to be ready...": "Esperando a que el servicio esté listo...", + "Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.", + "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.", + "Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado", + "Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.", "Core Version Updated": "Versión del núcleo actualizada", "Clash Core Restarted": "Núcleo de Clash reiniciado", "GeoData Updated": "GeoData actualizado", @@ -523,7 +524,6 @@ "Unknown": "Desconocido", "Auto update disabled": "La actualización automática está deshabilitada", "Update subscription successfully": "Suscripción actualizada con éxito", - "Update failed, retrying with Clash proxy...": "Error al actualizar la suscripción. Intentando con el proxy de Clash...", "Update with Clash proxy successfully": "Actualización con el proxy de Clash exitosa", "Update failed even with Clash proxy": "Error al actualizar incluso con el proxy de Clash", "Profile creation failed, retrying with Clash proxy...": "Error al crear la suscripción. Intentando con el proxy de Clash...", @@ -555,10 +555,9 @@ "Disallowed ISP": "Proveedor de servicios de Internet no permitido", "Originals Only": "Solo originales", "Unsupported Country/Region": "País/región no soportado", + "Configuration saved successfully": "Configuración aleatoria guardada correctamente", "Controller address copied to clipboard": "El puerto API se copió al portapapeles", "Secret copied to clipboard": "La clave API se copió al portapapeles", "Copy to clipboard": "Haz clic aquí para copiar", - "Port Config": "Configuración de puerto", - "Configuration saved successfully": "Configuración aleatoria guardada correctamente", "Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave" } diff --git a/src/locales/fa.json b/src/locales/fa.json index 3d529254..346394f9 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -21,7 +21,6 @@ "Label-Connections": "اتصالات", "Label-Rules": "قوانین", "Label-Logs": "لاگ‌ها", - "Label-Test": "آزمون", "Label-Settings": "تنظیمات", "Proxies": "پراکسی‌ها", "Proxy Groups": "گروه‌های پراکسی", @@ -166,7 +165,6 @@ "Table View": "نمای جدولی", "List View": "نمای لیستی", "Close All": "بستن همه", - "Default": "پیش‌فرض", "Download Speed": "سرعت دانلود", "Upload Speed": "سرعت بارگذاری", "Host": "میزبان", @@ -197,6 +195,8 @@ "Settings": "تنظیمات", "System Setting": "تنظیمات سیستم", "Tun Mode": "Tun (کارت شبکه مجازی)", + "TUN requires Service Mode": "حالت تونل‌زنی نیاز به سرویس دارد", + "Install Service": "نصب سرویس", "Reset to Default": "بازنشانی به پیش‌فرض", "Tun Mode Info": "حالت Tun (NIC مجازی): تمام ترافیک سیستم را ضبط می کند، وقتی فعال باشد، نیازی به فعال کردن پروکسی سیستم نیست.", "Stack": "انباشته Tun", @@ -275,13 +275,13 @@ "Restart": "راه‌اندازی مجدد", "Release Version": "نسخه نهایی", "Alpha Version": "نسخه آلفا", - "Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید", "Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید", "Grant": "اعطا", "Open UWP tool": "باز کردن ابزار UWP", "Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود", "Update GeoData": "به‌روزرسانی GeoData", - "Verge Setting": "تنظیمات Verge", + "Verge Basic Setting": "تنظیمات پایه Verge", + "Verge Advanced Setting": "تنظیمات پیشرفته Verge", "Language": "زبان", "Theme Mode": "حالت تم", "theme.light": "روشن", @@ -368,6 +368,7 @@ "Profile Reactivated": "پروفایل مجدداً فعال شد", "Only YAML Files Supported": "فقط فایل‌های YAML پشتیبانی می‌شوند", "Settings Applied": "تنظیمات اعمال شد", + "Installing Service...": "در حال نصب سرویس...", "Service Installed Successfully": "سرویس با موفقیت نصب شد", "Service Uninstalled Successfully": "سرویس با موفقیت حذف نصب شد", "Proxy Daemon Duration Cannot be Less than 1 Second": "مدت زمان دیمن پراکسی نمی‌تواند کمتر از 1 ثانیه باشد", @@ -451,10 +452,9 @@ "Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد", "Core Changed Successfully": "هسته با موفقیت تغییر کرد", "Failed to Change Core": "تغییر هسته ناموفق بود", - "Verge Basic Setting": "تنظیمات پایه Verge", - "Verge Advanced Setting": "تنظیمات پیشرفته Verge", - "TUN requires Service Mode": "حالت تونل‌زنی نیاز به سرویس دارد", - "Install Service": "نصب سرویس", - "Installing Service...": "در حال نصب سرویس...", - "Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد" + "Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد", + "Default": "پیش‌فرض", + "Label-Test": "آزمون", + "Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید", + "Verge Setting": "تنظیمات Verge" } diff --git a/src/locales/id.json b/src/locales/id.json index 82fc74d8..7d15a601 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -21,40 +21,7 @@ "Label-Connections": "Koneksi", "Label-Rules": "Aturan", "Label-Logs": "Log", - "Label-Test": "Tes", "Label-Settings": "Pengaturan", - "Dashboard": "Dasbor", - "Profile": "Profil", - "Help": "Bantuan", - "About": "Tentang", - "Theme": "Tema", - "Main Window": "Jendela Utama", - "Group Icon": "Ikon Grup", - "Menu Icon": "Ikon Menu", - "PAC File": "Berkas PAC", - "Web UI": "Antarmuka Web", - "Hotkeys": "Pintasan", - "Verge Mixed Port": "Port Campuran Verge", - "Verge Socks Port": "Port Socks Verge", - "Verge Redir Port": "Port Pengalihan Verge", - "Verge Tproxy Port": "Port Tproxy Verge", - "Verge Port": "Port Verge", - "Verge HTTP Enabled": "HTTP Verge Diaktifkan", - "WebDAV URL": "URL WebDAV", - "WebDAV Username": "Nama Pengguna WebDAV", - "WebDAV Password": "Kata Sandi WebDAV", - "Restart App": "Mulai Ulang Aplikasi", - "Restart Clash Core": "Mulai Ulang Core Clash", - "TUN Mode": "Mode TUN", - "Copy Env": "Salin Env", - "Conf Dir": "Direktori Konfigurasi", - "Core Dir": "Direktori Core", - "Logs Dir": "Direktori Log", - "Open Dir": "Buka Direktori", - "More": "Lainnya", - "Rule Mode": "Mode Aturan", - "Global Mode": "Mode Global", - "Direct Mode": "Mode Langsung", "Proxies": "Proksi", "Proxy Groups": "Grup Proksi", "Proxy Provider": "Penyedia Proksi", @@ -198,7 +165,6 @@ "Table View": "Tampilan Tabel", "List View": "Tampilan Daftar", "Close All": "Tutup Semua", - "Default": "Default", "Download Speed": "Kecepatan Unduh", "Upload Speed": "Kecepatan Unggah", "Host": "Host", @@ -229,6 +195,8 @@ "Settings": "Pengaturan", "System Setting": "Pengaturan Sistem", "Tun Mode": "Mode Tun (NIC Virtual)", + "TUN requires Service Mode": "Mode TUN memerlukan layanan", + "Install Service": "Instal Layanan", "Reset to Default": "Setel Ulang ke Default", "Tun Mode Info": "Mode Tun (NIC Virtual): Menangkap semua lalu lintas sistem, saat diaktifkan, tidak perlu mengaktifkan proksi sistem.", "Stack": "Tumpukan Tun", @@ -312,7 +280,8 @@ "Open UWP tool": "Buka alat UWP", "Open UWP tool Info": "Sejak Windows 8, aplikasi UWP (seperti Microsoft Store) dibatasi dari mengakses layanan jaringan host lokal secara langsung, dan alat ini dapat digunakan untuk melewati pembatasan ini", "Update GeoData": "Perbarui GeoData", - "Verge Setting": "Pengaturan Verge", + "Verge Basic Setting": "Pengaturan Dasar Verge", + "Verge Advanced Setting": "Pengaturan Lanjutan Verge", "Language": "Bahasa", "Theme Mode": "Mode Tema", "theme.light": "Terang", @@ -399,6 +368,7 @@ "Profile Reactivated": "Profil Diaktifkan Kembali", "Only YAML Files Supported": "Hanya File YAML yang Didukung", "Settings Applied": "Pengaturan Diterapkan", + "Installing Service...": "Memasang Layanan...", "Service Installed Successfully": "Layanan Berhasil Diinstal", "Service Uninstalled Successfully": "Layanan Berhasil Dicopot", "Proxy Daemon Duration Cannot be Less than 1 Second": "Durasi Daemon Proksi Tidak Boleh Kurang dari 1 Detik", @@ -437,6 +407,38 @@ "Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?", "Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik", "Failed to fetch backup files": "Gagal mengambil file cadangan", + "Profile": "Profil", + "Help": "Bantuan", + "About": "Tentang", + "Theme": "Tema", + "Main Window": "Jendela Utama", + "Group Icon": "Ikon Grup", + "Menu Icon": "Ikon Menu", + "PAC File": "Berkas PAC", + "Web UI": "Antarmuka Web", + "Hotkeys": "Pintasan", + "Verge Mixed Port": "Port Campuran Verge", + "Verge Socks Port": "Port Socks Verge", + "Verge Redir Port": "Port Pengalihan Verge", + "Verge Tproxy Port": "Port Tproxy Verge", + "Verge Port": "Port Verge", + "Verge HTTP Enabled": "HTTP Verge Diaktifkan", + "WebDAV URL": "URL WebDAV", + "WebDAV Username": "Nama Pengguna WebDAV", + "WebDAV Password": "Kata Sandi WebDAV", + "Dashboard": "Dasbor", + "Restart App": "Mulai Ulang Aplikasi", + "Restart Clash Core": "Mulai Ulang Core Clash", + "TUN Mode": "Mode TUN", + "Copy Env": "Salin Env", + "Conf Dir": "Direktori Konfigurasi", + "Core Dir": "Direktori Core", + "Logs Dir": "Direktori Log", + "Open Dir": "Buka Direktori", + "More": "Lainnya", + "Rule Mode": "Mode Aturan", + "Global Mode": "Mode Global", + "Direct Mode": "Mode Langsung", "Enable Tray Speed": "Aktifkan Tray Speed", "LightWeight Mode": "Mode Ringan", "LightWeight Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan", @@ -450,10 +452,8 @@ "Script File Error": "Kesalahan file skrip, perubahan dibatalkan", "Core Changed Successfully": "Inti berhasil diubah", "Failed to Change Core": "Gagal mengubah inti", - "Verge Basic Setting": "Pengaturan Dasar Verge", - "Verge Advanced Setting": "Pengaturan Lanjutan Verge", - "TUN requires Service Mode": "Mode TUN memerlukan layanan", - "Install Service": "Instal Layanan", - "Installing Service...": "Memasang Layanan...", - "Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem" + "Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem", + "Default": "Default", + "Label-Test": "Tes", + "Verge Setting": "Pengaturan Verge" } diff --git a/src/locales/jp.json b/src/locales/jp.json index 1d4ad133..618e4fbd 100644 --- a/src/locales/jp.json +++ b/src/locales/jp.json @@ -195,10 +195,11 @@ "Settings": "設定", "System Setting": "システム設定", "Tun Mode": "仮想ネットワークカードモード", - "TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です", "Install Service": "サービスをインストール", + "Uninstall Service": "サービスのアンインストール", "Reset to Default": "デフォルト値にリセット", "Tun Mode Info": "TUN(仮想ネットワークカード)モードはシステムのすべてのトラフィックを制御します。有効にすると、システムプロキシを開く必要はありません。", + "TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です", "System Proxy Enabled": "システムプロキシが有効になっています。アプリケーションはプロキシを通じてネットワークにアクセスします。", "System Proxy Disabled": "システムプロキシが無効になっています。ほとんどのユーザーはこのオプションをオンにすることをお勧めします。", "TUN Mode Enabled": "TUNモードが有効になっています。アプリケーションは仮想ネットワークカードを通じてネットワークにアクセスします。", @@ -254,6 +255,7 @@ "Unified Delay Info": "統一遅延を有効にすると、2回の遅延テストが行われ、接続ハンドシェイクなどによる異なるタイプのノードの遅延差を解消します。", "Log Level": "ログレベル", "Log Level Info": "ログディレクトリのServiceフォルダ内のコアログファイルにのみ適用されます。", + "Port Config": "ポート設定", "Random Port": "ランダムポート", "Mixed Port": "混合プロキシポート", "Socks Port": "SOCKSプロキシポート", @@ -370,17 +372,16 @@ "Stopping Core...": "コアを停止中...", "Restarting Core...": "コアを再起動中...", "Installing Service...": "サービスをインストール中...", - "Uninstall Service": "サービスのアンインストール", - "Service Installed Successfully": "サービスのインストールに成功しました。", - "Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。", - "Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。", - "Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。", - "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。", "Uninstalling Service...": "サービスをアンインストール中...", - "Waiting for service to be ready...": "サービスの準備を待っています...", + "Service Installed Successfully": "サービスのインストールに成功しました。", "Service Uninstalled Successfully": "サービスのアンインストールに成功しました。", "Proxy Daemon Duration Cannot be Less than 1 Second": "プロキシデーモンの間隔は1秒以上に設定する必要があります。", "Invalid Bypass Format": "無効なバイパス形式", + "Waiting for service to be ready...": "サービスの準備を待っています...", + "Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。", + "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。", + "Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。", + "Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。", "Core Version Updated": "コアバージョンが更新されました。", "Clash Core Restarted": "Clashコアが再起動されました。", "GeoData Updated": "GeoDataが更新されました。", @@ -526,7 +527,6 @@ "Unknown": "不明", "Auto update disabled": "自動更新が無効になっています。", "Update subscription successfully": "サブスクリプションの更新に成功しました。", - "Update failed, retrying with Clash proxy...": "サブスクリプションの更新に失敗しました。Clashプロキシを使用して再試行します...", "Update with Clash proxy successfully": "Clashプロキシを使用して更新に成功しました。", "Update failed even with Clash proxy": "Clashプロキシを使用しても更新に失敗しました。", "Profile creation failed, retrying with Clash proxy...": "プロファイルの作成に失敗しました。Clashプロキシを使用して再試行します...", @@ -558,12 +558,9 @@ "Disallowed ISP": "許可されていないインターネットサービスプロバイダー", "Originals Only": "オリジナルのみ", "Unsupported Country/Region": "サポートされていない国/地域", + "Configuration saved successfully": "ランダム設定を保存完了", "Controller address copied to clipboard": "API ポートがクリップボードにコピーされました", "Secret copied to clipboard": "API キーがクリップボードにコピーされました", - "Copy to clipboard": "クリックしてコピー", - "Port Config": "ポート設定", - "Configuration saved successfully": "ランダム設定を保存完了", - "Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください", "Batch Operations": "バッチ操作", "Delete Selected Profiles": "選択したプロファイルを削除", "Deselect All": "すべての選択を解除", @@ -571,5 +568,7 @@ "items": "アイテム", "Select All": "すべて選択", "Selected": "選択済み", - "Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました" + "Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました", + "Copy to clipboard": "クリックしてコピー", + "Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください" } diff --git a/src/locales/ko.json b/src/locales/ko.json index 6c7a6073..b5b29571 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -127,6 +127,7 @@ "Lazy": "지연 로딩", "Timeout": "타임아웃", "Max Failed Times": "최대 실패 횟수", + "Interface Name": "인터페이스 이름", "Routing Mark": "라우팅 마크", "Include All": "모든 프록시 및 제공자 포함", "Include All Providers": "모든 제공자 포함", @@ -198,176 +199,52 @@ "Edit Test": "테스트 편집", "Icon": "아이콘", "Test URL": "테스트 URL", - "Timeout (ms)": "타임아웃 (ms)", - "Expected": "예상됨", - "URL": "URL", - "Method": "메소드", - "Failed": "실패", - "Succeed": "성공", "Settings": "설정", - "Core Config": "코어 설정", - "Clash Setting": "Clash 설정", - "Verge Setting": "Verge 설정", "System Setting": "시스템 설정", - "Appearance": "외관", - "Experimental Features": "실험적 기능", - "Others": "기타", - "Mixed Port": "혼합 포트", - "Allow LAN": "LAN 허용", - "IPv6": "IPv6", - "Log Level": "로그 레벨", - "Core Type": "코어 유형", - "General": "일반", - "Mode": "모드", "Tun Mode": "Tun 모드", - "Transparent Proxy": "투명 프록시", - "Specify YAML": "YAML 지정", - "Status": "상태", - "Memory Usage": "메모리 사용량", "Stack": "스택", - "Network": "네트워크", - "MTU": "MTU", "Auto Route": "자동 라우팅", "Auto Detect Interface": "인터페이스 자동 감지", - "Interface Name": "인터페이스 이름", - "Endpoint Independent Nat": "엔드포인트 독립 NAT", - "Include Reserved": "예약된 IP 포함", - "Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화", - "TCP Fast Open": "TCP 빠른 열기", - "Silent Start": "자동 시작", - "TcpConcurrent": "TCP 동시성", + "MTU": "MTU", "Service Mode": "서비스 모드", "System Proxy": "시스템 프록시", - "Start With System": "시스템과 함께 시작", - "Set System Proxy": "시스템 프록시 설정", - "Set as System Proxy": "시스템 프록시로 설정", - "System Proxy Status": "시스템 프록시 상태", - "Start Option": "시작 옵션", - "Start Core on Start": "시작 시 코어 시작", - "Start Core with System": "시스템과 함께 코어 시작", - "Start Core with System Proxy": "시스템 프록시와 함께 코어 시작", - "Start Core with Tun": "Tun과 함께 코어 시작", - "Silent Start Option": "자동 시작 옵션", - "Hidden Window on Start": "시작 시 창 숨기기", - "Log Notice": "로그 알림", - "Warning": "경고", - "Error": "오류", + "Silent Start": "자동 시작", + "Clash Setting": "Clash 설정", + "IPv6": "IPv6", + "Log Level": "로그 레벨", + "Mixed Port": "혼합 포트", "Verge Basic Setting": "Verge 기본 설정", "Language": "언어", "Theme Mode": "테마 모드", "Tray Click Event": "트레이 클릭 이벤트", "Show Main Window": "메인 창 표시", "Show Tray Menu": "트레이 메뉴 표시", - "Open Config Folder": "설정 폴더 열기", - "Open Dashboard": "대시보드 열기", - "Hotkey Setting": "단축키 설정", - "Misc Setting": "기타 설정", - "Layout Setting": "레이아웃 설정", - "Update Setting": "업데이트 설정", - "Enable Hotkeys": "단축키 활성화", - "System Hotkey": "시스템 단축키", - "Hotkey Enable": "단축키 활성화", - "Require Clash Core Running": "Clash 코어 실행 필요", "Copy Env Type": "환경 유형 복사", + "Copy Success": "복사 성공", "Start Page": "시작 페이지", "Startup Script": "시작 스크립트", - "Icon Group Type": "아이콘 그룹 유형", - "Always": "항상", - "On Update": "업데이트 시", - "By Traffic": "트래픽별", - "Web UI List": "웹 UI 목록", - "Installed Web UI": "설치된 웹 UI", - "Built-in Web UI": "내장 웹 UI", - "Current Config": "현재 설정", - "System Config": "시스템 설정", - "Port": "포트", - "WebUI Current Port": "웹 UI 현재 포트", - "Theme": "테마", - "Light": "라이트", - "Dark": "다크", - "Auto": "자동", - "System": "시스템", - "Proxy Item Width": "프록시 항목 너비", - "Proxy Item Height": "프록시 항목 높이", - "Compact Mode": "압축 모드", - "Git Proxy": "Git 프록시", - "Enable API": "API 활성화", - "Enable Lan": "LAN 활성화", - "Select a config file": "설정 파일 선택", - "Open Config Dir": "설정 디렉토리 열기", - "System Proxy Permission": "시스템 프록시 권한", - "System Stack Type": "시스템 스택 유형", - "Undefined stack": "정의되지 않은 스택", - "Auto Start": "자동 시작", - "Mixin": "혼합", - "Set as System Auto Proxy": "시스템 자동 프록시로 설정", - "System Auto Proxy Status": "시스템 자동 프록시 상태", - "Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)", + "Primary Color": "기본 색상", + "Layout Setting": "레이아웃 설정", + "Traffic Graph": "트래픽 그래프", + "Memory Usage": "메모리 사용량", + "Auto Delay Detection": "자동 지연 감지", + "Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다", + "Hotkey Setting": "단축키 설정", + "Filter": "필터", + "Import Subscription Successful": "구독 가져오기 성공", "Username": "사용자 이름", "Password": "비밀번호", - "Auth Proxy": "인증 프록시", - "Geox User": "Geox 사용자", - "Geox Password": "Geox 비밀번호", - "Log File": "로그 파일", - "Enable Clash.Meta Logs": "Clash.Meta 로그 활성화", - "Verge Log": "Verge 로그", - "Enable Verge Logs": "Verge 로그 활성화", - "Traffic Graph": "트래픽 그래프", - "Profile Token": "프로필 토큰", - "Profile User Agent": "프로필 사용자 에이전트", - "The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.", - "Profile Format": "프로필 포맷", - "The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.", - "Theme Color": "테마 색상", - "Primary Color": "기본 색상", - "Customize primary color": "기본 색상 사용자 정의", - "Danger Zone": "위험 영역", - "Reset Verge Theme": "Verge 테마 재설정", - "Inject CSS": "CSS 주입", - "Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입", - "Inject HTML": "HTML 주입", - "Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)", - "Capture": "캡처", - "Color Scheme": "색상 구성표", - "Default": "기본값", - "Pink": "분홍색", - "Red": "빨간색", - "Yellow": "노란색", - "Green": "녹색", - "Cyan": "청록색", - "Blue": "파란색", - "Purple": "보라색", - "Proxy Detail": "프록시 상세", - "Address": "주소", - "Filter": "필터", - "Check Updates on Start": "시작 시 업데이트 확인", - "For Alpha Version": "알파 버전용", - "Latest Build Version": "최신 빌드 버전", - "Check Updates": "업데이트 확인", - "Proxy Setting": "프록시 설정", - "WebDav Setting": "WebDav 설정", - "WebDav Upload": "WebDav 업로드", - "WebDav Download": "WebDav 다운로드", - "Clean Cache": "캐시 정리", - "Check Network": "네트워크 확인", - "WebDav Status": "WebDav 상태", - "WebDav URL": "WebDav URL", - "WebDav Username": "WebDav 사용자 이름", - "WebDav Password": "WebDav 비밀번호", - "Update Interval(minute)": "업데이트 간격(분)", - "Skip Cert Verify": "인증서 확인 건너뛰기", - "Import Subscription Successful": "구독 가져오기 성공", - "Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공", - "Update failed, retrying with Clash proxy...": "업데이트 실패, Clash 프록시로 재시도 중...", - "Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패", + "Theme": "테마", + "Config Validation Failed": "설정 검증 실패", "Boot Config Validation Failed": "부팅 설정 검증 실패", "Core Change Config Validation Failed": "코어 변경 설정 검증 실패", - "Config Validation Failed": "설정 검증 실패", "Config Validation Process Terminated": "설정 검증 프로세스 종료됨", - "Script File Error": "스크립트 파일 오류", "Script Syntax Error": "스크립트 구문 오류", "Script Missing Main": "스크립트 메인 없음", "File Not Found": "파일을 찾을 수 없음", + "Script File Error": "스크립트 파일 오류", + "Core Changed Successfully": "코어 변경 성공", + "Failed to Change Core": "코어 변경 실패", "YAML Syntax Error": "YAML 구문 오류", "YAML Read Error": "YAML 읽기 오류", "YAML Mapping Error": "YAML 매핑 오류", @@ -377,28 +254,150 @@ "Merge File Mapping Error": "병합 파일 매핑 오류", "Merge File Key Error": "병합 파일 키 오류", "Merge File Error": "병합 파일 오류", - "Core Changed Successfully": "코어 변경 성공", - "Failed to Change Core": "코어 변경 실패", - "Copy Success": "복사 성공", - "Copy Failed": "복사 실패", + "Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공", + "Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패", + "Failed": "실패", + "Address": "주소", + "Allow LAN": "LAN 허용", + "Always": "항상", + "Appearance": "외관", + "Auth Proxy": "인증 프록시", + "Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)", + "Auto": "자동", + "Auto Start": "자동 시작", + "Blue": "파란색", + "Built-in Web UI": "내장 웹 UI", + "By Traffic": "트래픽별", "Cannot Import Empty Subscription URL": "빈 구독 URL을 가져올 수 없습니다", - "Profile Already Exists": "프로필이 이미 존재합니다", - "Input Subscription URL": "구독 URL 입력", - "Create Profile Successful": "프로필 생성 성공", + "Capture": "캡처", + "Check Network": "네트워크 확인", + "Check Updates": "업데이트 확인", + "Check Updates on Start": "시작 시 업데이트 확인", + "Clean Cache": "캐시 정리", + "Color Scheme": "색상 구성표", + "Compact Mode": "압축 모드", + "Copy Failed": "복사 실패", + "Core Config": "코어 설정", + "Core Type": "코어 유형", "Create Profile Failed": "프로필 생성 실패", - "Patch Profile Successful": "프로필 패치 성공", - "Patch Profile Failed": "프로필 패치 실패", - "Delete Profile Successful": "프로필 삭제 성공", + "Create Profile Successful": "프로필 생성 성공", + "Current Config": "현재 설정", + "Customize primary color": "기본 색상 사용자 정의", + "Cyan": "청록색", + "Danger Zone": "위험 영역", + "Dark": "다크", + "Default": "기본값", "Delete Profile Failed": "프로필 삭제 실패", - "Select Active Profile Successful": "활성 프로필 선택 성공", + "Delete Profile Successful": "프로필 삭제 성공", + "Enable API": "API 활성화", + "Enable Clash.Meta Logs": "Clash.Meta 로그 활성화", + "Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화", + "Enable Hotkeys": "단축키 활성화", + "Enable Lan": "LAN 활성화", + "Enable Verge Logs": "Verge 로그 활성화", + "Endpoint Independent Nat": "엔드포인트 독립 NAT", + "Error": "오류", + "Expected": "예상됨", + "Experimental Features": "실험적 기능", + "For Alpha Version": "알파 버전용", + "General": "일반", + "Geox Password": "Geox 비밀번호", + "Geox User": "Geox 사용자", + "Git Proxy": "Git 프록시", + "Green": "녹색", + "Hidden Window on Start": "시작 시 창 숨기기", + "Hotkey Enable": "단축키 활성화", + "Icon Group Type": "아이콘 그룹 유형", + "Include Reserved": "예약된 IP 포함", + "Inject CSS": "CSS 주입", + "Inject HTML": "HTML 주입", + "Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입", + "Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)", + "Input Subscription URL": "구독 URL 입력", + "Installed Web UI": "설치된 웹 UI", + "Latest Build Version": "최신 빌드 버전", + "Light": "라이트", + "Log File": "로그 파일", + "Log Notice": "로그 알림", + "Method": "메소드", + "Misc Setting": "기타 설정", + "Mixin": "혼합", + "Mode": "모드", + "Network": "네트워크", + "On Update": "업데이트 시", + "Open Config Dir": "설정 디렉토리 열기", + "Open Config Folder": "설정 폴더 열기", + "Open Dashboard": "대시보드 열기", + "Others": "기타", + "Patch Profile Failed": "프로필 패치 실패", + "Patch Profile Successful": "프로필 패치 성공", + "Pink": "분홍색", + "Port": "포트", + "Profile Already Exists": "프로필이 이미 존재합니다", + "Profile Format": "프로필 포맷", + "Profile Token": "프로필 토큰", + "Profile User Agent": "프로필 사용자 에이전트", + "Proxy Detail": "프록시 상세", + "Proxy Item Height": "프록시 항목 높이", + "Proxy Item Width": "프록시 항목 너비", + "Proxy Setting": "프록시 설정", + "Purple": "보라색", + "Red": "빨간색", + "Require Clash Core Running": "Clash 코어 실행 필요", + "Reset Verge Theme": "Verge 테마 재설정", "Select Active Profile Failed": "활성 프로필 선택 실패", - "View Profile-Runtime": "프로필-런타임 보기", - "View Profile-Content": "프로필-내용 보기", - "View Profile-Original": "프로필-원본 보기", - "View Profile-Script": "프로필-스크립트 보기", - "View Profile-Merge": "프로필-병합 보기", - "Update Successful": "업데이트 성공", + "Select Active Profile Successful": "활성 프로필 선택 성공", + "Select a config file": "설정 파일 선택", + "Set System Proxy": "시스템 프록시 설정", + "Set as System Auto Proxy": "시스템 자동 프록시로 설정", + "Set as System Proxy": "시스템 프록시로 설정", + "Silent Start Option": "자동 시작 옵션", + "Skip Cert Verify": "인증서 확인 건너뛰기", + "Specify YAML": "YAML 지정", + "Start Core on Start": "시작 시 코어 시작", + "Start Core with System": "시스템과 함께 코어 시작", + "Start Core with System Proxy": "시스템 프록시와 함께 코어 시작", + "Start Core with Tun": "Tun과 함께 코어 시작", + "Start Option": "시작 옵션", + "Start With System": "시스템과 함께 시작", + "Status": "상태", + "Succeed": "성공", + "System": "시스템", + "System Auto Proxy Status": "시스템 자동 프록시 상태", + "System Config": "시스템 설정", + "System Hotkey": "시스템 단축키", + "System Proxy Permission": "시스템 프록시 권한", + "System Proxy Status": "시스템 프록시 상태", + "System Stack Type": "시스템 스택 유형", + "TCP Fast Open": "TCP 빠른 열기", + "TcpConcurrent": "TCP 동시성", + "The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.", + "The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.", + "Theme Color": "테마 색상", + "Timeout (ms)": "타임아웃 (ms)", + "Transparent Proxy": "투명 프록시", + "URL": "URL", + "Undefined stack": "정의되지 않은 스택", "Update Failed": "업데이트 실패", - "Auto Delay Detection": "자동 지연 감지", - "Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다" + "Update Interval(minute)": "업데이트 간격(분)", + "Update Setting": "업데이트 설정", + "Update Successful": "업데이트 성공", + "Verge Log": "Verge 로그", + "Verge Setting": "Verge 설정", + "View Profile-Content": "프로필-내용 보기", + "View Profile-Merge": "프로필-병합 보기", + "View Profile-Original": "프로필-원본 보기", + "View Profile-Runtime": "프로필-런타임 보기", + "View Profile-Script": "프로필-스크립트 보기", + "Warning": "경고", + "Web UI List": "웹 UI 목록", + "WebDav Download": "WebDav 다운로드", + "WebDav Password": "WebDav 비밀번호", + "WebDav Setting": "WebDav 설정", + "WebDav Status": "WebDav 상태", + "WebDav URL": "WebDav URL", + "WebDav Upload": "WebDav 업로드", + "WebDav Username": "WebDav 사용자 이름", + "WebUI Current Port": "웹 UI 현재 포트", + "Yellow": "노란색" } diff --git a/src/locales/tr.json b/src/locales/tr.json index 35f8dc0e..0b406488 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -508,7 +508,6 @@ "Fake IP Filter Mode": "Sahte IP Filtre Modu", "Enable IPv6 DNS resolution": "IPv6 DNS çözümlemesini etkinleştir", "Prefer H3": "H3'ü Tercih Et", - "DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır", "Respect Rules": "Kurallara Uy", "DNS connections follow routing rules": "DNS bağlantıları yönlendirme kurallarını takip eder", "Use Hosts": "Hosts Kullan", @@ -575,7 +574,6 @@ "Unknown": "Bilinmiyor", "Auto update disabled": "Otomatik güncelleme devre dışı", "Update subscription successfully": "Abonelik başarıyla güncellendi", - "Update failed, retrying with Clash proxy...": "Güncelleme başarısız oldu, Clash vekil ile yeniden deneniyor...", "Update with Clash proxy successfully": "Clash vekil ile güncelleme başarılı", "Update failed even with Clash proxy": "Clash vekil ile bile güncelleme başarısız oldu", "Profile creation failed, retrying with Clash proxy...": "Profil oluşturma başarısız oldu, Clash vekil ile yeniden deneniyor...", @@ -616,5 +614,6 @@ "items": "öğeler", "Select All": "Tümünü Seç", "Selected": "Seçildi", - "Selected profiles deleted successfully": "Seçili profiller başarıyla silindi" + "Selected profiles deleted successfully": "Seçili profiller başarıyla silindi", + "DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır" } diff --git a/src/locales/tt.json b/src/locales/tt.json index 8f9a96d0..2a4d153f 100644 --- a/src/locales/tt.json +++ b/src/locales/tt.json @@ -21,7 +21,6 @@ "Label-Connections": "Тоташулар", "Label-Rules": "Кагыйдәләр", "Label-Logs": "Логлар", - "Label-Test": "Тест", "Label-Settings": "Көйләүләр", "Proxies": "Прокси", "Proxy Groups": "Прокси төркемнәре", @@ -166,7 +165,6 @@ "Table View": "Таблица күзаллау", "List View": "Исемлек күзаллау", "Close All": "Барысын да ябу", - "Default": "Башлангыч", "Download Speed": "Йөкләү тизлеге", "Upload Speed": "Йөкләү (чыгару) тизлеге", "Host": "Хост", @@ -197,6 +195,8 @@ "Settings": "Көйләүләр", "System Setting": "Система көйләүләре", "Tun Mode": "Tun режимы (виртуаль челтәр адаптеры)", + "TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә", + "Install Service": "Хезмәтне урнаштыру", "Reset to Default": "Башлангычка кайтару", "Tun Mode Info": "Tun режимы бөтен системаның трафигын тотып ала. Аны кабызган очракта системалы проксины аерым кабызу таләп ителми.", "Stack": "Стек", @@ -280,7 +280,8 @@ "Open UWP tool": "UWP инструментын ачу", "Open UWP tool Info": "Windows 8'дән башлап UWP кушымталары (Microsoft Store кебек) локаль хосттагы челтәр хезмәтләренә турыдан-туры тоташа алмый. Бу инструмент әлеге чикләүне әйләнеп узарга ярдәм итә", "Update GeoData": "GeoData яңарту", - "Verge Setting": "Verge көйләүләре", + "Verge Basic Setting": "Verge Төп көйләүләр", + "Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр", "Language": "Тел", "Theme Mode": "Теманың режимы", "theme.light": "Якты", @@ -296,8 +297,6 @@ "Theme Setting": "Тема көйләүләре", "Primary Color": "Төп төс", "Secondary Color": "Икенче төс", - "Primary Text Color": "Төп текст төсе", - "Secondary Text Color": "Икенче текст төсе", "Info Color": "Мәгълүмат төсе", "Warning Color": "Кисәтү төсе", "Error Color": "Хата төсе", @@ -367,6 +366,7 @@ "Profile Reactivated": "Профиль яңадан активлаштырылды", "Only YAML Files Supported": "Фәкать YAML-файллар гына хуплана", "Settings Applied": "Көйләүләр кулланылды", + "Installing Service...": "Хезмәт урнаштырыла...", "Service Installed Successfully": "Сервис уңышлы урнаштырылды", "Service Uninstalled Successfully": "Сервис уңышлы салдырылды", "Proxy Daemon Duration Cannot be Less than 1 Second": "Прокси-демон эш вакыты 1 секундтан ким була алмый", @@ -380,7 +380,6 @@ "Clash Core Restarted": "Clash ядросы яңадан башланды", "GeoData Updated": "GeoData яңартылды", "Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган", - "Import subscription successful": "Подписка уңышлы импортланды", "WebDAV Server URL": "WebDAV сервер URL-ы (http(s)://)", "Username": "Кулланучы исеме", "Password": "Пароль", @@ -450,10 +449,11 @@ "Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды", "Core Changed Successfully": "Ядро уңышлы алыштырылды", "Failed to Change Core": "Ядро алыштыру уңышсыз булды", - "Verge Basic Setting": "Verge Төп көйләүләр", - "Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр", - "TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә", - "Install Service": "Хезмәтне урнаштыру", - "Installing Service...": "Хезмәт урнаштырыла...", - "Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә" + "Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә", + "Default": "Башлангыч", + "Import subscription successful": "Подписка уңышлы импортланды", + "Label-Test": "Тест", + "Primary Text Color": "Төп текст төсе", + "Secondary Text Color": "Икенче текст төсе", + "Verge Setting": "Verge көйләүләре" } diff --git a/src/locales/zh.json b/src/locales/zh.json index 1ecd9919..f8b161d7 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -305,7 +305,7 @@ "Socks Port": "SOCKS 代理端口", "Http Port": "HTTP(S) 代理端口", "Redir Port": "Redir 透明代理端口", - "TPROXY Port": "TPROXY 透明代理端口", + "Tproxy Port": "Tproxy 透明代理端口", "Port settings saved": "端口设置已保存", "Failed to save port settings": "端口设置保存失败", "External": "外部控制", @@ -426,6 +426,8 @@ "Uninstalling Service...": "卸载服务中...", "Service Installed Successfully": "已成功安装服务", "Service Uninstalled Successfully": "已成功卸载服务", + "Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒", + "Invalid Bypass Format": "无效的代理绕过格式", "Waiting for service to be ready...": "等待服务准备就绪...", "Service not ready, retrying attempt {count}/{total}...": "服务未就绪,正在重试 {{count}}/{{total}} 次...", "Failed to check service status, retrying attempt {count}/{total}...": "检查服务状态失败,正在重试 {{count}}/{{total}} 次...", @@ -436,8 +438,6 @@ "Fallback core restart also failed: {message}": "后备内核重启也失败了: {{message}}", "Service is ready and core restarted": "服务已就绪,内核已重启", "Core restarted. Service is now available.": "内核已重启,服务现已可用", - "Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒", - "Invalid Bypass Format": "无效的代理绕过格式", "Clash Port Modified": "Clash 端口已修改", "Port Conflict": "端口冲突", "Restart Application to Apply Modifications": "重启 Verge 以应用修改", @@ -628,7 +628,6 @@ "Unknown": "未知", "Auto update disabled": "自动更新已禁用", "Update subscription successfully": "订阅更新成功", - "Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新", "Update with Clash proxy successfully": "使用 Clash 代理更新成功", "Update failed even with Clash proxy": "使用 Clash 代理更新也失败", "Profile creation failed, retrying with Clash proxy...": "订阅创建失败,尝试使用 Clash 代理创建", @@ -713,5 +712,8 @@ "Allow Auto Update": "允许自动更新", "Menu reorder mode": "菜单排序模式", "Unlock menu order": "解锁菜单排序", - "Lock menu order": "锁定菜单排序" + "Lock menu order": "锁定菜单排序", + "Open App Log": "应用日志", + "Open Core Log": "内核日志", + "TPROXY Port": "TPROXY 透明代理端口" } diff --git a/src/locales/zhtw.json b/src/locales/zhtw.json index 491272b1..efcbba3c 100644 --- a/src/locales/zhtw.json +++ b/src/locales/zhtw.json @@ -24,6 +24,7 @@ "Label-Logs": "日 誌", "Label-Unlock": "解 鎖", "Label-Settings": "設 定", + "Proxies": "代理", "Proxy Groups": "代理組", "Proxy Chain Mode": "鏈式代理模式", "Connect": "連線", @@ -37,8 +38,16 @@ "rule": "規則", "global": "全域", "direct": "直連", - "script": "指令碼", "Chain Proxy": "🔗 鏈式代理", + "Chain Proxy Config": "鏈式代理設定", + "Proxy Rules": "代理規則", + "Select Rules": "选择规则", + "Click nodes in order to add to proxy chain": "依序點擊節點新增到鏈式代理中", + "No proxy chain configured": "暫無鏈式代理設定", + "Proxy Order": "代理順序", + "timeout": "逾時", + "Clear All": "全部清除", + "script": "指令碼", "locate": "目前節點", "Delay check": "延遲測試", "Sort by default": "預設排序", @@ -144,6 +153,8 @@ "Group Name Already Exists": "代理組名稱已存在", "Extend Config": "擴充覆寫設定", "Extend Script": "擴充指令碼", + "Global Merge": "全域擴充覆寫設定", + "Global Script": "全域擴充指令碼", "Type": "類型", "Name": "名稱", "Descriptions": "描述", @@ -152,6 +163,7 @@ "Choose File": "選擇檔案", "Use System Proxy": "使用系統代理更新", "Use Clash Proxy": "使用內核代理更新", + "Accept Invalid Certs (Danger)": "允許無效憑證(危險)", "Refresh": "重整", "Home": "首頁", "Select": "使用", @@ -159,13 +171,18 @@ "Edit File": "編輯檔案", "Open File": "開啟檔案", "Update": "更新", + "Update via proxy": "更新(代理)", + "Update(Proxy)": "更新(代理)", "Confirm deletion": "確認刪除", "This operation is not reversible": "此操作無法復原", "Script Console": "指令碼控制台輸出", + "To Top": "前往頂端", + "To End": "前往末端", "Connections": "連線", "Table View": "表格檢視", "List View": "列表檢視", "Close All": "關閉全部", + "Close All Connections": "關閉全部連線", "Upload": "上傳", "Download": "下載", "Download Speed": "下載速度", @@ -199,29 +216,46 @@ "Test URL": "測試網址", "Settings": "設定", "System Setting": "系統設定", - "Tun Mode": "虛擬網卡模式", - "TUN requires Service Mode or Admin Mode": "虛擬網卡模式需要服務模式或管理員模式", + "Tun Mode": "虛擬網路介面卡模式", "Install Service": "安裝服務", + "Install Service failed": "安裝服務失敗", + "Uninstall Service": "解除安裝服務", + "Restart Core failed": "重新啟動內核失敗", "Reset to Default": "重設為預設值", "Tun Mode Info": "TUN(虛擬網路介面卡)模式接管系統所有流量,啟用時無需開啟系統代理", + "TUN requires Service Mode or Admin Mode": "虛擬網路介面卡模式需要安裝服務模式或以系統管理員身分執行", + "TUN Mode automatically disabled due to service unavailable": "由於服務不可使用,虛擬網路介面卡模式已自動停用", + "Failed to disable TUN Mode automatically": "自動停用虛擬網路介面卡模式失敗", "System Proxy Enabled": "系統代理已啟用,您的應用程式將透過代理存取網路", "System Proxy Disabled": "系統代理已關閉,建議大多數使用者開啟此選項", - "TUN Mode Enabled": "虛擬網卡模式已啟用,應用程式將透過虛擬網路介面卡存取網路", - "TUN Mode Disabled": "虛擬網卡模式已關閉,適用於特殊應用程式", - "TUN Mode Service Required": "虛擬網卡模式需要服務模式,請先安裝服務", - "TUN Mode Intercept Info": "虛擬網卡模式可以接管所有應用程式流量,適用於不遵循系統代理設定的特殊應用程式", - "Stack": "虛擬網卡模式堆疊", - "Device": "TUN 網路介面卡名稱", + "TUN Mode Enabled": "虛擬網路介面卡模式已啟用,應用程式將透過虛擬網路介面卡存取網路", + "TUN Mode Disabled": "虛擬網路介面卡模式已關閉,適用於特殊應用程式", + "TUN Mode Service Required": "虛擬網路介面卡模式需要服務模式,請先安裝服務", + "TUN Mode Intercept Info": "虛擬網路介面卡模式可以接管所有應用程式流量,適用於不遵循系統代理設定的特殊應用程式", + "Core communication error": "內核通信錯誤", + "Rule Mode Description": "基於預設規則智慧判斷流量走向,提供更靈活的代理策略", + "Global Mode Description": "所有流量均透過代理伺服器,適用於需要全域科學上網的場合", + "Direct Mode Description": "所有流量不透過代理伺服器,但經過Clash內核轉發連線目標伺服器,適用於需要透過內核進行分流的特定場合", + "Stack": "虛擬網路介面卡模式堆疊", + "System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服務模式下使用", + "Device": "虛擬網路介面卡名稱", "Auto Route": "自動設定全域路由", "Strict Route": "嚴格路由", "Auto Detect Interface": "自動偵測流量輸出介面", "DNS Hijack": "DNS 綁架", "MTU": "最大傳輸單位", "Service Mode": "服務模式", + "Service Mode Info": "啟用虛擬網路介面卡模式前請先安裝服務模式,該服務啟動的內核處理程序可以獲得安裝虛擬網路介面卡(TUN 模式)的權限", + "Current State": "当前状态", + "pending": "等待中", + "installed": "已安装", + "uninstall": "未安装", "active": "作用中", "unknown": "未知", + "Information: Please make sure that the Clash Verge Service is installed and enabled": "提示資訊:請確認 Clash Verge Service 已安裝並啟用", "Install": "安裝", "Uninstall": "解除安裝", + "Disable Service Mode": "停用服務模式", "System Proxy": "系統代理", "System Proxy Info": "修改作業系統的代理設定,如果開啟失敗,可手動修改作業系統的代理設定", "System Proxy Setting": "系統代理設定", @@ -237,6 +271,7 @@ "Proxy Guard Info": "開啟以防止其他軟體修改作業系統的代理設定", "Guard Duration": "代理守護間隔", "Always use Default Bypass": "始終使用預設繞過", + "Use Bypass Check": "啟用代理繞過檢查", "Proxy Bypass": "代理繞過設定:", "Bypass": "目前繞過:", "Use PAC Mode": "使用 PAC 模式", @@ -246,6 +281,10 @@ "Administrator mode may not support auto launch": "管理員模式可能不支援開機自啟", "Silent Start": "靜默啟動", "Silent Start Info": "程序啟動時以後台模式執行,不顯示程序面板", + "Hover Jump Navigator": "懸浮跳轉導航", + "Hover Jump Navigator Info": "滑鼠懸停在字母上時自動捲動到對應代理組", + "Hover Jump Navigator Delay": "懸浮跳轉導航延遲", + "Hover Jump Navigator Delay Info": "滑鼠懸停後觸發自動跳轉前等待的毫秒數", "TG Channel": "Telegram 頻道", "Manual": "使用手冊", "Github Repo": "GitHub 專案位址", @@ -259,13 +298,16 @@ "Unified Delay Info": "開啟統一延遲時,會進行兩次延遲測試,以消除連線握手等帶來的不同類型節點的延遲差異", "Log Level": "日誌等級", "Log Level Info": "僅對日誌目錄 Service 資料夾下的內核日誌檔案生效", + "Port Config": "連接埠設定", "Random Port": "隨機連接埠", "Mixed Port": "混合代理連接埠", "Socks Port": "SOCKS 代理連接埠", "Http Port": "HTTP(S) 代理連接埠", "Redir Port": "Redir 透明代理連接埠", - "Tproxy Port": "TPROXY 透明代理連接埠", + "Port settings saved": "連結埠設定已儲存", + "Failed to save port settings": "連結埠設定儲存失敗", "External": "外部控制", + "Enable External Controller": "啟用外部控制器", "External Controller": "外部控制器監聽位址", "Core Secret": "API 存取金鑰", "Recommended": "建議設定", @@ -277,7 +319,9 @@ "Restart": "重啟內核", "Release Version": "正式版", "Alpha Version": "預覽版", + "Please Enable Service Mode": "請先安裝並啟用服務模式", "Please enter your root password": "請輸入您的 root 密碼", + "Grant": "授權", "Open UWP tool": "UWP 工具", "Open UWP tool Info": "Windows 8 開始限制 UWP 應用程式(如Microsoft Store)直接存取本機主機的網路服務,使用此工具可繞過該限制", "Update GeoData": "更新 GeoData", @@ -312,17 +356,13 @@ "Memory Usage": "內核佔用", "Memory Cleanup": "點擊清理記憶體", "Proxy Group Icon": "代理組圖示", - "Hover Jump Navigator": "懸浮跳轉導航", - "Hover Jump Navigator Info": "滑鼠懸停在字母上時自動捲動到對應代理組", - "Hover Jump Navigator Delay": "懸浮跳轉導航延遲", - "Hover Jump Navigator Delay Info": "滑鼠懸停後觸發自動跳轉前等待的毫秒數", "Nav Icon": "導覽列圖示", "Monochrome": "單色圖示", "Colorful": "彩色圖示", "Tray Icon": "系統匣圖示", "Common Tray Icon": "一般系統匣圖示", "System Proxy Tray Icon": "系統代理系統匣圖示", - "Tun Tray Icon": "虛擬網卡模式系統匣圖示", + "Tun Tray Icon": "虛擬網路介面卡模式系統匣圖示", "Miscellaneous": "雜項設定", "App Log Level": "應用程式日誌等級", "Auto Close Connections": "自動關閉連線", @@ -347,7 +387,7 @@ "clash_mode_global": "全域模式", "clash_mode_direct": "直連模式", "toggle_system_proxy": "開啟/關閉系統代理", - "toggle_tun_mode": "開啟/關閉 虛擬網卡模式", + "toggle_tun_mode": "開啟/關閉 虛擬網路介面卡模式", "entry_lightweight_mode": "進入輕量模式", "Backup Setting": "備份設定", "Backup Setting Info": "支援本機或 WebDAV 方式備份配置檔案", @@ -362,6 +402,7 @@ "Break Change Update Error": "此版本為重大更新,不支援應用程式內更新,請解除安裝後手動下載安裝", "Open Dev Tools": "開發人員工具", "Export Diagnostic Info": "匯出診斷資訊", + "Export Diagnostic Info For Issue Reporting": "匯出診斷資訊用於問題回報", "Exit": "離開", "Verge Version": "Verge 版本", "ReadOnly": "唯讀", @@ -374,26 +415,37 @@ "Profile Imported Successfully": "匯入設定檔成功", "Profile Switched": "訂閱已切換", "Profile Reactivated": "訂閱已啟用", + "Profile switch interrupted by new selection": "配置切換被新的選擇中斷", "Only YAML Files Supported": "僅支援 YAML 檔案", "Settings Applied": "設定已套用", "Stopping Core...": "內核停止中...", "Restarting Core...": "內核重啟中...", "Installing Service...": "安裝服務中...", - "Uninstall Service": "解除安裝服務", - "Service Installed Successfully": "已成功安裝服務", - "Service is ready and core restarted": "服務已就緒,內核已重啟", - "Core restarted. Service is now available.": "內核已重啟,服務已就緒", - "Service was ready, but core restart might have issues or service became unavailable. Please check.": "服務已就緒,但內核重啟可能存在問題或服務已不可用。請檢查。", - "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "服務安裝或內核重啟遇到問題。服務可能不可用。請檢查系統日誌。", "Uninstalling Service...": "服務解除安裝中...", - "Waiting for service to be ready...": "等待服務就緒...", + "Service Installed Successfully": "已成功安裝服務", "Service Uninstalled Successfully": "已成功解除安裝服務", "Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒", "Invalid Bypass Format": "無效的代理繞過格式", + "Waiting for service to be ready...": "等待服務就緒...", + "Service not ready, retrying attempt {count}/{total}...": "服務未就緒,正在重試 {{count}}/{{total}} 次...", + "Failed to check service status, retrying attempt {count}/{total}...": "檢查服務狀態失敗,正在重試 {{count}}/{{total}} 次...", + "Service did not become ready after attempts. Proceeding with core restart.": "服務在嘗試後仍未就緒,正在重新啟動內核。", + "Service was ready, but core restart might have issues or service became unavailable. Please check.": "服務已就緒,但內核重啟可能存在問題或服務已不可用。請檢查。", + "Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "服務安裝或內核重啟遇到問題。服務可能不可用。請檢查系統日誌。", + "Attempting to restart core as a fallback...": "嘗試重新啟動內核作為備援方案...", + "Fallback core restart also failed: {message}": "被園內核重新啟動也失敗了:{{message}}", + "Service is ready and core restarted": "服務已就緒,內核已重啟", + "Core restarted. Service is now available.": "內核已重啟,服務已就緒", + "Clash Port Modified": "Clash 連結埠已修改", + "Port Conflict": "連結埠衝突", + "Restart Application to Apply Modifications": "重新啟動 Verge 以套用修改", + "External Controller Address Modified": "外部控制器監聽位址已修改", + "Permissions Granted Successfully for _clash Core": "{{core}} 內核授權成功", "Core Version Updated": "內核版本已更新", "Clash Core Restarted": "已重啟 Clash 內核", "GeoData Updated": "已更新 GeoData", "Currently on the Latest Version": "目前已是最新版本", + "Already Using Latest Core": "已是最新內核版本", "Import Subscription Successful": "匯入訂閱成功", "WebDAV Server URL": "WebDAV 伺服器位址 http(s)://", "Username": "使用者名稱", @@ -411,6 +463,7 @@ "Invalid WebDAV URL": "無效的 WebDAV 伺服器位址格式", "Username Required": "使用者名稱不能為空", "Password Required": "密碼不能為空", + "Failed to Fetch Backups": "取得備份檔案失敗", "WebDAV Config Saved": "WebDAV 配置儲存成功", "WebDAV Config Save Failed": "儲存 WebDAV 配置失敗: {{error}}", "Backup Created": "備份建立成功", @@ -424,14 +477,33 @@ "Export Backup": "匯出備份", "Restore Backup": "還原備份", "Backup Time": "備份時間", + "Confirm to delete this backup file?": "確認是否刪除此備份檔案嗎?", + "Confirm to restore this backup file?": "確認還原此份檔案嗎?", "Restore Success, App will restart in 1s": "還原成功,應用程式將在 1 秒後重啟", "Failed to fetch backup files": "取得備份檔案失敗", - "Profile": "配置檔案", + "Profile": "配置", + "Help": "幫助", + "About": "關於", + "Theme": "主題", + "Main Window": "主視窗", + "Group Icon": "群組圖示", + "Menu Icon": "選單圖示", + "PAC File": "PAC 檔案", "Web UI": "網頁介面", + "Hotkeys": "快捷键", + "Verge Mixed Port": "Verge 混合連結埠", + "Verge Socks Port": "Verge SOCKS 連結埠", + "Verge Redir Port": "Verge 重新導向連結埠", + "Verge Tproxy Port": "Verge 透明代理連結埠", + "Verge Port": "Verge 連結埠", + "Verge HTTP Enabled": "Verge HTTP 已啟用", + "WebDAV URL": "WebDAV 位址", + "WebDAV Username": "WebDAV 用戶名", + "WebDAV Password": "WebDAV 密碼", "Dashboard": "儀表板", "Restart App": "重啟應用程式", "Restart Clash Core": "重啟 Clash 內核", - "TUN Mode": "虛擬網卡模式", + "TUN Mode": "虛擬網路介面卡模式", "Copy Env": "複製環境變數", "Conf Dir": "配置目錄", "Core Dir": "內核目錄", @@ -471,6 +543,10 @@ "Merge File Mapping Error": "覆寫檔案映射錯誤,變更已撤銷", "Merge File Key Error": "覆寫檔案鍵錯誤,變更已撤銷", "Merge File Error": "覆寫檔案錯誤,變更已撤銷", + "Validate YAML File": "驗證YAML檔案", + "Validate Merge File": "驗證覆寫檔案", + "Validation Success": "驗證成功", + "Validation Failed": "驗證失敗", "Service Administrator Prompt": "Clash Verge 需要管理員權限安裝系統服務", "DNS Settings": "DNS 設定", "DNS settings saved": "DNS 設定已儲存", @@ -517,11 +593,14 @@ "Hosts Settings": "Hosts 設定", "Hosts": "Hosts", "Custom domain to IP or domain mapping": "自訂網域到 IP 或網域的映射,用逗號分隔", + "Enable Alpha Channel": "啟用 Alpha 頻道", + "Alpha versions may contain experimental features and bugs": "Alpha 版本可能內含實驗性功能和已知問題,常有不穩定的情況發生。", "Home Settings": "首頁設定", "Profile Card": "訂閱卡", "Current Proxy Card": "目前代理卡", "Network Settings Card": "網路設定卡", "Proxy Mode Card": "代理模式卡", + "Clash Mode Card": "Clash模式卡", "Traffic Stats Card": "流量統計卡", "Clash Info Cards": "Clash 資訊卡", "System Info Cards": "系統資訊卡", @@ -538,6 +617,7 @@ "Running Mode": "執行模式", "Sidecar Mode": "使用者模式", "Administrator Mode": "管理員模式", + "Administrator + Service Mode": "系統管理員 + 服務模式", "Last Check Update": "最後檢查更新", "Click to import subscription": "點擊匯入訂閱", "Last Update failed": "上次更新失敗", @@ -546,7 +626,6 @@ "Unknown": "未知", "Auto update disabled": "自動更新已停用", "Update subscription successfully": "訂閱更新成功", - "Update failed, retrying with Clash proxy...": "訂閱更新失敗,嘗試使用 Clash 代理更新", "Update with Clash proxy successfully": "使用 Clash 代理更新成功", "Update failed even with Clash proxy": "使用 Clash 代理更新也失敗", "Profile creation failed, retrying with Clash proxy...": "訂閱建立失敗,嘗試使用 Clash 代理建立", @@ -577,11 +656,60 @@ "Completed": "檢測完成", "Disallowed ISP": "不允許的網際網路服務供應商", "Originals Only": "僅限原創", + "No (IP Banned By Disney+)": "不支援(IP被Disney+禁止)", "Unsupported Country/Region": "不支援的國家/地區", + "Failed (Network Connection)": "測試失敗(網路連線問題)", + "DashboardToggledTitle": "儀錶板已切換", + "DashboardToggledBody": "已透過快速鍵切換儀錶板顯示狀態", + "ClashModeChangedTitle": "Clash模式切換", + "ClashModeChangedBody": "已切換為 {mode} 模式", + "SystemProxyToggledTitle": "系統代理切換", + "SystemProxyToggledBody": "已透過快速鍵切換系統代理狀態", + "TunModeToggledTitle": "TUN模式切換", + "TunModeToggledBody": "已透過快速鍵切換TUN模式", + "LightweightModeEnteredTitle": "輕量模式", + "LightweightModeEnteredBody": "已透過快速鍵進入輕量模式", + "AppQuitTitle": "應用程式退出", + "AppQuitBody": "已透過快速鍵退出應用程式", + "AppHiddenTitle": "應用程式隱藏", + "AppHiddenBody": "已透過快速鍵隱藏應用程式視窗", + "Invalid Profile URL": "無效的訂閱網址,請輸入以 http:// 或 https:// 開頭的位址", + "Saved Successfully": "儲存成功", + "External Cors": "外部跨來源資源共享", + "Enable one-click CORS for external API. Click to toggle CORS": "設定內核跨來源存取,點擊切換跨來源資源共享是否啟用", + "External Cors Settings": "外部跨來源資源共享設定", + "External Cors Configuration": "外部跨來源資源共享設定", + "Allow private network access": "允許專用網路存取", + "Allowed Origins": "允許的來源", + "Please enter a valid url": "請輸入有效的網址", + "Add": "新增", + "Always included origins: {{urls}}": "始終包含來源:{{urls}}", + "Invalid regular expression": "無效的正規表示式", + "Copy Version": "複製Verge版本號", + "Version copied to clipboard": "Verge版本已複製到剪貼簿", + "Controller address cannot be empty": "控制器位址不能為空", + "Secret cannot be empty": "存取金鑰不能為空", + "Configuration saved successfully": "設定儲存完成", + "Failed to save configuration": "設定儲存失敗", "Controller address copied to clipboard": "API 連接埠已複製到剪貼簿", - "Secret copied to clipboard": "API 密鑰已複製到剪貼簿", - "Copy to clipboard": "點擊我複製到剪貼簿", - "Port Config": "連接埠設定", - "Configuration saved successfully": "配置儲存完成", - "Enable one-click random API port and key. Click to randomize the port and key": "開啟一鍵隨機 API 連接埠和密鑰,點擊即可隨機化連接埠和密鑰" + "Secret copied to clipboard": "API 金鑰已複製到剪貼簿", + "Saving...": "儲存中...", + "Proxy node already exists in chain": "該節點已在鏈式代理表中", + "Detection timeout or failed": "檢測逾時或失敗", + "Batch Operations": "批次操作", + "Delete Selected Profiles": "刪除選取訂閱", + "Deselect All": "取消選取", + "Done": "完成", + "items": "項目", + "Select All": "全選", + "Selected": "已選取", + "Selected profiles deleted successfully": "選取的訂閱已成功刪除", + "Prefer System Titlebar": "優先使用系統標題欄", + "App Log Max Size": "應用程式日誌最大大小", + "App Log Max Count": "應用程式日誌最大數量", + "Allow Auto Update": "允許自動更新", + "Menu reorder mode": "選單排序模式", + "Unlock menu order": "解鎖選單排序", + "Lock menu order": "鎖定選單排序", + "TPROXY Port": "TPROXY 透明代理連接埠" } diff --git a/src/pages/_layout/notificationHandlers.ts b/src/pages/_layout/notificationHandlers.ts index 0f69ce30..afa674c8 100644 --- a/src/pages/_layout/notificationHandlers.ts +++ b/src/pages/_layout/notificationHandlers.ts @@ -19,13 +19,13 @@ export const handleNoticeMessage = ( showNotice("error", msg); }, "set_config::error": () => showNotice("error", msg), + // 后端暂时没有启用相关通知, 批量更新可能造成扰人提醒 + // update_success: () => showNotice("success", t("Update subscription successfully")), update_with_clash_proxy: () => showNotice( "success", `${t("Update with Clash proxy successfully")} ${msg}`, ), - update_retry_with_clash: () => - showNotice("info", t("Update failed, retrying with Clash proxy...")), update_failed_even_with_clash: () => showNotice( "error", diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 597ec4ec..0643c3d0 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -99,112 +99,6 @@ const isOperationAborted = ( return false; }; -const normalizeProfileUrl = (value?: string) => { - if (!value) return ""; - const trimmed = value.trim(); - - try { - const url = new URL(trimmed); - const auth = - url.username || url.password - ? `${url.username}${url.password ? `:${url.password}` : ""}@` - : ""; - const normalized = - `${url.protocol.toLowerCase()}//${auth}${url.hostname.toLowerCase()}` + - `${url.port ? `:${url.port}` : ""}${url.pathname}${url.search}${url.hash}`; - - return normalized.replace(/\/+$/, ""); - } catch { - const schemeNormalized = trimmed.replace( - /^([a-z]+):\/\//i, - (match, scheme: string) => `${scheme.toLowerCase()}://`, - ); - return schemeNormalized.replace(/\/+$/, ""); - } -}; - -const getProfileSignature = (profile?: IProfileItem | null) => { - if (!profile) return ""; - const { extra, selected, option, name, desc } = profile; - return JSON.stringify({ - extra: extra ?? null, - selected: selected ?? null, - option: option ?? null, - name: name ?? null, - desc: desc ?? null, - }); -}; - -type ImportLandingVerifier = { - baselineCount: number; - hasLanding: (config?: IProfilesConfig | null) => boolean; -}; - -const createImportLandingVerifier = ( - items: IProfileItem[] | undefined, - url: string, -): ImportLandingVerifier => { - const normalizedUrl = normalizeProfileUrl(url); - const baselineCount = items?.length ?? 0; - const baselineProfile = normalizedUrl - ? items?.find((item) => normalizeProfileUrl(item?.url) === normalizedUrl) - : undefined; - const baselineSignature = getProfileSignature(baselineProfile); - const baselineUpdated = baselineProfile?.updated ?? 0; - const hadBaselineProfile = Boolean(baselineProfile); - - const hasLanding = (config?: IProfilesConfig | null) => { - const currentItems = config?.items ?? []; - const currentCount = currentItems.length; - - if (currentCount > baselineCount) { - console.log( - `[导入验证] 配置数量已增加: ${baselineCount} -> ${currentCount}`, - ); - return true; - } - - if (!normalizedUrl) { - return false; - } - - const matchingProfile = currentItems.find( - (item) => normalizeProfileUrl(item?.url) === normalizedUrl, - ); - - if (!matchingProfile) { - return false; - } - - if (!hadBaselineProfile) { - console.log("[导入验证] 检测到新的订阅记录,判定为导入成功"); - return true; - } - - const currentSignature = getProfileSignature(matchingProfile); - const currentUpdated = matchingProfile.updated ?? 0; - - if (currentUpdated > baselineUpdated) { - console.log( - `[导入验证] 订阅更新时间已更新 ${baselineUpdated} -> ${currentUpdated}`, - ); - return true; - } - - if (currentSignature !== baselineSignature) { - console.log("[导入验证] 订阅详情发生变化,判定为导入成功"); - return true; - } - - return false; - }; - - return { - baselineCount, - hasLanding, - }; -}; - const ProfilePage = () => { const { t } = useTranslation(); const location = useLocation(); @@ -382,55 +276,19 @@ const ProfilePage = () => { } setLoading(true); - const importVerifier = createImportLandingVerifier(profiles?.items, url); - const handleImportSuccess = async (noticeKey: string) => { showNotice("success", t(noticeKey)); setUrl(""); - await performRobustRefresh(importVerifier); - }; - - const waitForImportLanding = async () => { - const maxChecks = 2; - for (let attempt = 0; attempt <= maxChecks; attempt++) { - try { - const currentProfiles = await getProfiles(); - if (importVerifier.hasLanding(currentProfiles)) { - return true; - } - - if (attempt < maxChecks) { - await new Promise((resolve) => - setTimeout(resolve, 200 * (attempt + 1)), - ); - } - } catch (verifyErr) { - console.warn("[导入验证] 获取配置状态失败:", verifyErr); - break; - } - } - - return false; + await performRobustRefresh(); }; try { // 尝试正常导入 await importProfile(url); await handleImportSuccess("Profile Imported Successfully"); - return; } catch (initialErr) { console.warn("[订阅导入] 首次导入失败:", initialErr); - const alreadyImported = await waitForImportLanding(); - if (alreadyImported) { - console.warn( - "[订阅导入] 接口返回失败,但检测到订阅已导入,跳过回退导入流程", - ); - await handleImportSuccess("Profile Imported Successfully"); - return; - } - - // 首次导入失败且未检测到数据变更,尝试使用自身代理 showNotice("info", t("Import failed, retrying with Clash proxy...")); try { // 使用自身代理尝试导入 @@ -454,10 +312,7 @@ const ProfilePage = () => { }; // 强化的刷新策略 - const performRobustRefresh = async ( - importVerifier: ImportLandingVerifier, - ) => { - const { baselineCount, hasLanding } = importVerifier; + const performRobustRefresh = async () => { let retryCount = 0; const maxRetries = 5; const baseDelay = 200; @@ -477,28 +332,8 @@ const ProfilePage = () => { setTimeout(resolve, baseDelay * (retryCount + 1)), ); - // 验证刷新是否成功 - const currentProfiles = await getProfiles(); - const currentCount = currentProfiles?.items?.length || 0; - - if (currentCount > baselineCount) { - console.log( - `[导入刷新] 配置刷新成功,配置数量 ${baselineCount} -> ${currentCount}`, - ); - await onEnhance(false); - return; - } - - if (hasLanding(currentProfiles)) { - console.log("[导入刷新] 检测到订阅内容更新,判定刷新成功"); - await onEnhance(false); - return; - } - - console.warn( - `[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`, - ); - retryCount++; + await onEnhance(false); + return; } catch (error) { console.error(`[导入刷新] 第${retryCount + 1}次刷新失败:`, error); retryCount++; diff --git a/src/services/types.d.ts b/src/services/types.d.ts index 310723a9..fcac1137 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -803,7 +803,7 @@ interface IVergeConfig { sysproxy_tray_icon?: boolean; tun_tray_icon?: boolean; enable_tray_speed?: boolean; - enable_tray_icon?: boolean; + // enable_tray_icon?: boolean; tray_inline_proxy_groups?: boolean; enable_tun_mode?: boolean; enable_auto_light_weight_mode?: boolean; diff --git a/vite.config.mts b/vite.config.mts index 6082ee42..71305ec6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,15 +1,14 @@ import path from "node:path"; import legacy from "@vitejs/plugin-legacy"; -import react from "@vitejs/plugin-react"; +import react from "@vitejs/plugin-react-swc"; import monacoEditorPlugin, { type IMonacoEditorOpts, -} from "vite-plugin-monaco-editor"; +} from "vite-plugin-monaco-editor-esm"; import svgr from "vite-plugin-svgr"; import { defineConfig } from "vitest/config"; -const monacoEditorPluginDefault = (monacoEditorPlugin as any).default as ( - options: IMonacoEditorOpts, -) => any; +const monacoEditorPluginDefault = ((monacoEditorPlugin as any).default ?? + monacoEditorPlugin) as (options: IMonacoEditorOpts) => any; export default defineConfig({ root: "src",