Compare commits
66 Commits
renovate/c
...
chore/upda
77
.github/workflows/autobuild.yml
vendored
77
.github/workflows/autobuild.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.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)
|
||||
@@ -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
|
||||
@@ -466,6 +493,43 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-updater-manifests:
|
||||
name: Publish Updater Manifests
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
update_tag,
|
||||
autobuild-x86-windows-macos-linux,
|
||||
autobuild-arm-linux,
|
||||
autobuild-x86-arm-windows_webview2,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Publish updater manifests
|
||||
run: pnpm updater
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish WebView2 updater manifests
|
||||
run: pnpm updater-fixed-webview2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
notify-telegram:
|
||||
name: Notify Telegram
|
||||
runs-on: ubuntu-latest
|
||||
@@ -475,6 +539,7 @@ jobs:
|
||||
autobuild-x86-windows-macos-linux,
|
||||
autobuild-arm-linux,
|
||||
autobuild-x86-arm-windows_webview2,
|
||||
publish-updater-manifests,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -538,7 +603,7 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.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)
|
||||
@@ -551,7 +616,7 @@ jobs:
|
||||
- [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 }}-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)
|
||||
- [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)
|
||||
|
||||
12
.github/workflows/lint-clippy.yml
vendored
12
.github/workflows/lint-clippy.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
78
UPDATELOG.md
78
UPDATELOG.md
@@ -1,6 +1,45 @@
|
||||
## v2.4.3
|
||||
|
||||
### ✨ 新增功能
|
||||
感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- 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 系统主题切换不生效
|
||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||
- 修复轻量模式托盘状态不同步
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.15**
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
@@ -15,8 +54,11 @@
|
||||
- 允许独立控制订阅自动更新
|
||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
||||
</details>
|
||||
|
||||
### 🚀 优化改进
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||
- 重构并简化窗口创建流程
|
||||
@@ -43,36 +85,10 @@
|
||||
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
||||
- 添加热键绑定错误的提示信息
|
||||
- 在 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 渲染问题
|
||||
- 修复自动更新使版本回退的问题
|
||||
- 修复首页自定义卡片在切换轻量模式时失效
|
||||
- 修复悬浮跳转导航失效
|
||||
- 修复小键盘热键映射错误
|
||||
</details>
|
||||
|
||||
## v2.4.2
|
||||
|
||||
|
||||
81
docs/CONTRIBUTING_i18n.md
Normal file
81
docs/CONTRIBUTING_i18n.md
Normal file
@@ -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 <DIR>`
|
||||
- `-f, --file <FILE>`
|
||||
- `-k, --duplicated-key`
|
||||
- `-m, --missing-key`
|
||||
- `-e, --export <DIR>`
|
||||
- `-s, --sort`
|
||||
- `-b, --base <FILE>`
|
||||
|
||||
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 <DIR>`.
|
||||
- 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.
|
||||
41
package.json
41
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",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^8.15.0",
|
||||
"@mui/x-data-grid": "^8.16.0",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.1",
|
||||
"dayjs": "1.11.18",
|
||||
"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.2",
|
||||
"@types/node": "^24.10.0",
|
||||
"@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.4"
|
||||
"vitest": "^4.0.6"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
"eslint --fix",
|
||||
"eslint --fix --max-warnings=0",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
|
||||
931
pnpm-lock.yaml
generated
931
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,6 @@
|
||||
"groupName": "github actions"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
|
||||
"ignoreDeps": ["criterion"]
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
* pnpm release-version <version>
|
||||
*
|
||||
* <version> can be:
|
||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3-rc.1)
|
||||
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
|
||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
|
||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3-autobuild.1022.r2+cc39b2)
|
||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3-autobuild.1022.r2+a1b2c3d)
|
||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3-deploytest.1022.r2+cc39b2)
|
||||
*
|
||||
* Examples:
|
||||
* pnpm release-version 1.2.3
|
||||
@@ -30,10 +30,12 @@
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { program } from "commander";
|
||||
import fs from "fs/promises";
|
||||
import process from "node:process";
|
||||
import path from "path";
|
||||
|
||||
import { program } from "commander";
|
||||
|
||||
/**
|
||||
* 获取当前 git 短 commit hash
|
||||
* @returns {string}
|
||||
@@ -73,41 +75,118 @@ function getLatestTauriCommit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||
* 使用 Asia/Shanghai 时区
|
||||
* @param {boolean} withCommit 是否带 commit
|
||||
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效)
|
||||
* 获取 Asia/Shanghai 时区的日期片段
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
||||
function getLocalDatePart() {
|
||||
const now = new Date();
|
||||
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
const dateParts = Object.fromEntries(
|
||||
dateFormatter.formatToParts(now).map((part) => [part.type, part.value]),
|
||||
);
|
||||
|
||||
const parts = formatter.formatToParts(now);
|
||||
const month = parts.find((part) => part.type === "month").value;
|
||||
const day = parts.find((part) => part.type === "day").value;
|
||||
const month = dateParts.month ?? "00";
|
||||
const day = dateParts.day ?? "00";
|
||||
|
||||
if (withCommit) {
|
||||
const gitShort = useTauriCommit
|
||||
? getLatestTauriCommit()
|
||||
: getGitShortCommit();
|
||||
return `${month}${day}.${gitShort}`;
|
||||
}
|
||||
return `${month}${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GitHub Actions 运行编号(若存在)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getRunIdentifier() {
|
||||
const runNumber = process.env.GITHUB_RUN_NUMBER;
|
||||
if (runNumber && /^[0-9]+$/.test(runNumber)) {
|
||||
const runNum = Number.parseInt(runNumber, 10);
|
||||
if (!Number.isNaN(runNum)) {
|
||||
const base = `r${runNum.toString(36)}`;
|
||||
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
||||
if (attempt && /^[0-9]+$/.test(attempt)) {
|
||||
const attemptNumber = Number.parseInt(attempt, 10);
|
||||
if (!Number.isNaN(attemptNumber) && attemptNumber > 1) {
|
||||
return `${base}${attemptNumber.toString(36)}`;
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
||||
if (attempt && /^[0-9]+$/.test(attempt)) {
|
||||
const attemptNumber = Number.parseInt(attempt, 10);
|
||||
if (!Number.isNaN(attemptNumber)) {
|
||||
return `r${attemptNumber.toString(36)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成用于自动构建类渠道的版本后缀
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.includeCommit=false]
|
||||
* @param {"current"|"tauri"} [options.commitSource="current"]
|
||||
* @param {boolean} [options.includeRun=true]
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateChannelSuffix({
|
||||
includeCommit = false,
|
||||
commitSource = "current",
|
||||
includeRun = true,
|
||||
} = {}) {
|
||||
const segments = [];
|
||||
const date = getLocalDatePart();
|
||||
segments.push(date);
|
||||
|
||||
if (includeCommit) {
|
||||
const commit =
|
||||
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
||||
segments.push(commit);
|
||||
}
|
||||
|
||||
if (includeRun) {
|
||||
const run = getRunIdentifier();
|
||||
if (run) {
|
||||
segments.push(run);
|
||||
}
|
||||
}
|
||||
|
||||
return segments.join(".");
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 autobuild 渠道构建版本片段
|
||||
* @param {Object} options
|
||||
* @param {"current"|"tauri"} [options.commitSource="current"]
|
||||
* @returns {{date: string, run: string, metadata: string}}
|
||||
*/
|
||||
function generateAutobuildComponents({ commitSource = "current" } = {}) {
|
||||
const date = getLocalDatePart();
|
||||
const run = getRunIdentifier() ?? `manual${Date.now().toString(36)}`;
|
||||
const commitHash =
|
||||
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
||||
|
||||
return {
|
||||
date,
|
||||
run,
|
||||
metadata: commitHash || "nogit",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证版本号格式
|
||||
* @param {string} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidVersion(version) {
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
||||
version,
|
||||
);
|
||||
}
|
||||
@@ -122,13 +201,14 @@ function normalizeVersion(version) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取基础版本号(去掉所有 -tag 和 +build 部分)
|
||||
* 提取基础版本号(去掉所有 pre-release 和 build metadata)
|
||||
* @param {string} version
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBaseVersion(version) {
|
||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
||||
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
||||
const withoutBuild = cleaned.split("+")[0];
|
||||
const [base] = withoutBuild.split("-");
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -273,17 +353,17 @@ async function main(versionArg) {
|
||||
const baseVersion = getBaseVersion(currentVersion);
|
||||
|
||||
if (versionArg.toLowerCase() === "autobuild") {
|
||||
// 格式: 2.3.0+autobuild.1004.cc39b27
|
||||
// 使用 Tauri 相关的最新 commit hash
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
|
||||
// 格式: 2.3.0-autobuild.1022.r2+cc39b2
|
||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
||||
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
|
||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
||||
const latestTauriCommit = getLatestTauriCommit();
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
||||
// 格式: 2.3.0-autobuild.1022.r2+a1b2c3d (使用最新 Tauri 提交)
|
||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
||||
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
|
||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||
// 格式: 2.3.0+deploytest.1004.cc39b27
|
||||
// 使用 Tauri 相关的最新 commit hash
|
||||
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
|
||||
// 格式: 2.3.0-deploytest.1022.r2+cc39b2
|
||||
const parts = generateAutobuildComponents({ commitSource: "tauri" });
|
||||
newVersion = `${baseVersion}-deploytest.${parts.date}.${parts.run}+${parts.metadata}`;
|
||||
} else {
|
||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
|
||||
"\n<b>$1</b>\n",
|
||||
)
|
||||
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
|
||||
.replace(/<\/?details>/g, "")
|
||||
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
|
||||
.replace(/<br\s*\/?>/g, "\n");
|
||||
}
|
||||
|
||||
releaseContent = normalizeDetailsTags(releaseContent);
|
||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
|
||||
@@ -1,7 +1,105 @@
|
||||
import fetch from "node-fetch";
|
||||
import process from "node:process";
|
||||
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
|
||||
const SEMVER_REGEX =
|
||||
/v?\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/g;
|
||||
const STRICT_SEMVER_REGEX =
|
||||
/^\d+(?:\.\d+){2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
const stripLeadingV = (version) =>
|
||||
typeof version === "string" && version.startsWith("v")
|
||||
? version.slice(1)
|
||||
: version;
|
||||
|
||||
const preferCandidate = (current, candidate) => {
|
||||
if (!candidate) return current;
|
||||
if (!current) return candidate;
|
||||
|
||||
const candidateHasPre = /[-+]/.test(candidate);
|
||||
const currentHasPre = /[-+]/.test(current);
|
||||
|
||||
if (candidateHasPre && !currentHasPre) return candidate;
|
||||
if (candidateHasPre === currentHasPre && candidate.length > current.length) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const extractBestSemver = (input) => {
|
||||
if (typeof input !== "string") return null;
|
||||
const matches = input.match(SEMVER_REGEX);
|
||||
if (!matches) return null;
|
||||
|
||||
return matches
|
||||
.map(stripLeadingV)
|
||||
.reduce((best, candidate) => preferCandidate(best, candidate), null);
|
||||
};
|
||||
|
||||
const splitIdentifiers = (segment) =>
|
||||
segment
|
||||
.split(/[^0-9A-Za-z-]+/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const sanitizeSuffix = (value, fallbackLabel) => {
|
||||
if (!value) return fallbackLabel;
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return fallbackLabel;
|
||||
|
||||
const [preRelease = "", metadata] = trimmed.split("+", 2);
|
||||
const normalizedPre = splitIdentifiers(preRelease).join(".") || fallbackLabel;
|
||||
const normalizedMeta = metadata ? splitIdentifiers(metadata).join(".") : "";
|
||||
|
||||
return normalizedMeta ? `${normalizedPre}+${normalizedMeta}` : normalizedPre;
|
||||
};
|
||||
|
||||
const ensureSemverCompatibleVersion = (
|
||||
version,
|
||||
{ channel, releaseTag, fallbackLabel },
|
||||
) => {
|
||||
const trimmed = stripLeadingV(version ?? "").trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
if (STRICT_SEMVER_REGEX.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (channel === "autobuild") {
|
||||
const normalizedSuffix = sanitizeSuffix(trimmed, fallbackLabel ?? channel);
|
||||
const fallback = `0.0.0-${normalizedSuffix}`;
|
||||
console.warn(
|
||||
`[${channel}] Normalized non-semver version "${trimmed}" from release "${releaseTag}" to "${fallback}"`,
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[${channel}] Derived version "${trimmed}" is not semver compatible for release "${releaseTag}"`,
|
||||
);
|
||||
};
|
||||
|
||||
const resolveReleaseVersion = (release) => {
|
||||
const sources = [
|
||||
release?.name,
|
||||
release?.tag_name,
|
||||
release?.body,
|
||||
...(Array.isArray(release?.assets)
|
||||
? release.assets.map((asset) => asset?.name)
|
||||
: []),
|
||||
];
|
||||
|
||||
return sources.reduce((best, source) => {
|
||||
const candidate = extractBestSemver(source);
|
||||
return preferCandidate(best, candidate);
|
||||
}, null);
|
||||
};
|
||||
|
||||
// Add stable update JSON filenames
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
const UPDATE_JSON_FILE = "update.json";
|
||||
@@ -10,6 +108,11 @@ const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
const ALPHA_TAG_NAME = "updater-alpha";
|
||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
// Add autobuild update JSON filenames
|
||||
const AUTOBUILD_SOURCE_TAG_NAME = "autobuild";
|
||||
const AUTOBUILD_TAG_NAME = "updater-autobuild";
|
||||
const AUTOBUILD_UPDATE_JSON_FILE = "update.json";
|
||||
const AUTOBUILD_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
|
||||
/// generate update.json
|
||||
/// upload to update tag's release asset
|
||||
@@ -48,12 +151,12 @@ async function resolveUpdater() {
|
||||
|
||||
// More flexible tag detection with regex patterns
|
||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||
|
||||
// Get the latest stable tag and pre-release tag
|
||||
// Get tags for known channels
|
||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||
const autobuildTag = tags.find((t) => t.name === AUTOBUILD_SOURCE_TAG_NAME);
|
||||
|
||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||
@@ -61,32 +164,106 @@ async function resolveUpdater() {
|
||||
"Pre-release tag:",
|
||||
preReleaseTag ? preReleaseTag.name : "None found",
|
||||
);
|
||||
console.log(
|
||||
"Autobuild tag:",
|
||||
autobuildTag ? autobuildTag.name : "None found",
|
||||
);
|
||||
console.log();
|
||||
|
||||
// Process stable release
|
||||
if (stableTag) {
|
||||
await processRelease(github, options, stableTag, false);
|
||||
}
|
||||
const channels = [
|
||||
{
|
||||
name: "stable",
|
||||
tagName: stableTag?.name,
|
||||
updateReleaseTag: UPDATE_TAG_NAME,
|
||||
jsonFile: UPDATE_JSON_FILE,
|
||||
proxyFile: UPDATE_JSON_PROXY,
|
||||
prerelease: false,
|
||||
},
|
||||
{
|
||||
name: "alpha",
|
||||
tagName: preReleaseTag?.name,
|
||||
updateReleaseTag: ALPHA_TAG_NAME,
|
||||
jsonFile: ALPHA_UPDATE_JSON_FILE,
|
||||
proxyFile: ALPHA_UPDATE_JSON_PROXY,
|
||||
prerelease: true,
|
||||
},
|
||||
{
|
||||
name: "autobuild",
|
||||
tagName: autobuildTag?.name ?? AUTOBUILD_SOURCE_TAG_NAME,
|
||||
updateReleaseTag: AUTOBUILD_TAG_NAME,
|
||||
jsonFile: AUTOBUILD_UPDATE_JSON_FILE,
|
||||
proxyFile: AUTOBUILD_UPDATE_JSON_PROXY,
|
||||
prerelease: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Process pre-release if found
|
||||
if (preReleaseTag) {
|
||||
await processRelease(github, options, preReleaseTag, true);
|
||||
for (const channel of channels) {
|
||||
if (!channel.tagName) {
|
||||
console.log(`[${channel.name}] tag not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
await processRelease(github, options, channel);
|
||||
}
|
||||
}
|
||||
|
||||
// Process a release (stable or alpha) and generate update files
|
||||
async function processRelease(github, options, tag, isAlpha) {
|
||||
if (!tag) return;
|
||||
// Process a release and generate update files for the specified channel
|
||||
async function processRelease(github, options, channelConfig) {
|
||||
if (!channelConfig) return;
|
||||
|
||||
const {
|
||||
tagName,
|
||||
name: channelName,
|
||||
updateReleaseTag,
|
||||
jsonFile,
|
||||
proxyFile,
|
||||
prerelease,
|
||||
} = channelConfig;
|
||||
|
||||
const channelLabel =
|
||||
channelName.charAt(0).toUpperCase() + channelName.slice(1);
|
||||
|
||||
try {
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tag.name,
|
||||
tag: tagName,
|
||||
});
|
||||
|
||||
const releaseTagName = release.tag_name ?? tagName;
|
||||
const resolvedVersion = resolveReleaseVersion(release);
|
||||
|
||||
if (!resolvedVersion) {
|
||||
throw new Error(
|
||||
`[${channelName}] Failed to determine semver version from release "${releaseTagName}"`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${channelName}] Preparing update metadata from release "${releaseTagName}"`,
|
||||
);
|
||||
console.log(
|
||||
`[${channelName}] Resolved release version: ${resolvedVersion}`,
|
||||
);
|
||||
|
||||
const semverCompatibleVersion = ensureSemverCompatibleVersion(
|
||||
resolvedVersion,
|
||||
{
|
||||
channel: channelName,
|
||||
releaseTag: releaseTagName,
|
||||
fallbackLabel: channelName,
|
||||
},
|
||||
);
|
||||
|
||||
if (semverCompatibleVersion !== resolvedVersion) {
|
||||
console.log(
|
||||
`[${channelName}] Normalized updater version: ${semverCompatibleVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||
version: semverCompatibleVersion,
|
||||
original_version: resolvedVersion,
|
||||
tag_name: releaseTagName,
|
||||
notes: await resolveUpdateLog(releaseTagName).catch(() =>
|
||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||
),
|
||||
pub_date: new Date().toISOString(),
|
||||
@@ -186,13 +363,15 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log(updateData);
|
||||
console.log(`[${channelName}] Update data snapshot:`, updateData);
|
||||
|
||||
// maybe should test the signature as well
|
||||
// delete the null field
|
||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||
if (!value.url) {
|
||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||
console.log(
|
||||
`[${channelName}] [Error]: failed to parse release for "${key}"`,
|
||||
);
|
||||
delete updateData.platforms[key];
|
||||
}
|
||||
});
|
||||
@@ -205,15 +384,14 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
updateDataNew.platforms[key].url =
|
||||
"https://download.clashverge.dev/" + value.url;
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
console.log(
|
||||
`[${channelName}] [Error]: updateDataNew.platforms.${key} is null`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the appropriate updater release based on isAlpha flag
|
||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||
console.log(
|
||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
releaseTag,
|
||||
`[${channelName}] Processing update release target "${updateReleaseTag}"`,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -223,30 +401,28 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
// Try to get the existing release
|
||||
const response = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: releaseTag,
|
||||
tag: updateReleaseTag,
|
||||
});
|
||||
updateRelease = response.data;
|
||||
console.log(
|
||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
`[${channelName}] Found existing ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
// If release doesn't exist, create it
|
||||
if (error.status === 404) {
|
||||
console.log(
|
||||
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||
`[${channelName}] Release with tag ${updateReleaseTag} not found, creating new release...`,
|
||||
);
|
||||
const createResponse = await github.rest.repos.createRelease({
|
||||
...options,
|
||||
tag_name: releaseTag,
|
||||
name: isAlpha
|
||||
? "Auto-update Alpha Channel"
|
||||
: "Auto-update Stable Channel",
|
||||
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
||||
prerelease: isAlpha,
|
||||
tag_name: updateReleaseTag,
|
||||
name: `Auto-update ${channelLabel} Channel`,
|
||||
body: `This release contains the update information for the ${channelName} channel.`,
|
||||
prerelease,
|
||||
});
|
||||
updateRelease = createResponse.data;
|
||||
console.log(
|
||||
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
`[${channelName}] Created new ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} else {
|
||||
// If it's another error, throw it
|
||||
@@ -255,11 +431,8 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
}
|
||||
|
||||
// File names based on release type
|
||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||
|
||||
// Delete existing assets with these names
|
||||
for (let asset of updateRelease.assets) {
|
||||
for (const asset of updateRelease.assets) {
|
||||
if (asset.name === jsonFile) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
@@ -270,7 +443,12 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
if (asset.name === proxyFile) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
.catch((deleteError) =>
|
||||
console.error(
|
||||
`[${channelName}] Failed to delete existing proxy asset:`,
|
||||
deleteError.message,
|
||||
),
|
||||
); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,20 +468,22 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||
`[${channelName}] Successfully uploaded update files to ${updateReleaseTag}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
`[${channelName}] Failed to process update release:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||
console.log(
|
||||
`[${channelName}] Release not found for tag: ${tagName}, skipping...`,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to get release for tag: ${tag.name}`,
|
||||
`[${channelName}] Failed to get release for tag: ${tagName}`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
412
src-tauri/Cargo.lock
generated
412
src-tauri/Cargo.lock
generated
@@ -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 = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
@@ -479,7 +485,7 @@ dependencies = [
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"matchit 0.7.3",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
@@ -494,25 +500,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",
|
||||
@@ -538,19 +542,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",
|
||||
@@ -1068,18 +1070,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",
|
||||
@@ -1097,6 +1099,7 @@ version = "2.4.3"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
"backoff",
|
||||
"base64 0.22.1",
|
||||
@@ -1107,17 +1110,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",
|
||||
@@ -1136,7 +1135,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"sha2 0.10.9",
|
||||
"signal-hook 0.3.18",
|
||||
"smartstring",
|
||||
"sys-locale",
|
||||
@@ -1160,6 +1158,7 @@ dependencies = [
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"users",
|
||||
"warp",
|
||||
"winapi",
|
||||
@@ -1183,8 +1182,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash_verge_service_ipc"
|
||||
version = "2.0.20"
|
||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#c0b6e99da27e7956047d42aee104f5c33083c970"
|
||||
version = "2.0.21"
|
||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc#1e34c648e48f8580208ff777686092e0a94b8025"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"compact_str",
|
||||
@@ -1294,22 +1293,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",
|
||||
@@ -1318,14 +1318,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",
|
||||
@@ -3376,7 +3376,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3413,12 +3413,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",
|
||||
@@ -3426,9 +3427,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",
|
||||
@@ -3449,9 +3450,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",
|
||||
@@ -3471,14 +3472,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",
|
||||
@@ -3792,9 +3793,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",
|
||||
@@ -3987,9 +3988,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"
|
||||
@@ -4135,6 +4136,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"
|
||||
@@ -4226,9 +4233,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",
|
||||
@@ -5503,9 +5510,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",
|
||||
]
|
||||
@@ -5617,12 +5624,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]]
|
||||
@@ -5640,9 +5647,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",
|
||||
@@ -5662,11 +5669,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]]
|
||||
@@ -6050,11 +6057,11 @@ 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",
|
||||
]
|
||||
|
||||
@@ -6315,9 +6322,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",
|
||||
@@ -6325,9 +6332,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",
|
||||
@@ -7137,16 +7144,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",
|
||||
]
|
||||
|
||||
@@ -7990,11 +7997,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",
|
||||
]
|
||||
|
||||
@@ -8243,13 +8251,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",
|
||||
@@ -8261,11 +8268,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",
|
||||
@@ -8284,6 +8291,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"
|
||||
@@ -8332,11 +8350,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]]
|
||||
@@ -8616,9 +8638,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"
|
||||
@@ -8854,9 +8876,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",
|
||||
@@ -8865,25 +8887,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",
|
||||
@@ -8894,9 +8902,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",
|
||||
@@ -8904,22 +8912,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",
|
||||
]
|
||||
@@ -9012,9 +9020,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",
|
||||
@@ -9093,8 +9101,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]]
|
||||
@@ -9189,27 +9197,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]]
|
||||
@@ -9222,16 +9232,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]]
|
||||
@@ -9240,13 +9246,26 @@ 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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
@@ -9255,18 +9274,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]]
|
||||
@@ -9280,17 +9299,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"
|
||||
@@ -9324,6 +9332,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"
|
||||
@@ -9335,15 +9353,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"
|
||||
@@ -9354,13 +9363,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9372,6 +9380,15 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -9498,6 +9515,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"
|
||||
@@ -9714,16 +9740,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"
|
||||
@@ -9767,9 +9783,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"
|
||||
@@ -9867,9 +9883,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"
|
||||
@@ -9879,9 +9895,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"
|
||||
@@ -9891,11 +9907,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",
|
||||
@@ -9903,9 +9918,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",
|
||||
@@ -10038,9 +10053,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",
|
||||
@@ -10049,10 +10064,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",
|
||||
@@ -10060,9 +10076,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",
|
||||
@@ -10116,9 +10132,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",
|
||||
|
||||
@@ -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"
|
||||
@@ -67,11 +66,7 @@ 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.3"
|
||||
tokio-stream = "0.1.17"
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
@@ -82,15 +77,17 @@ backoff = { version = "0.4.0", features = ["tokio"] }
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
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.20", features = [
|
||||
clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
url = "2.5.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
|
||||
@@ -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<Box<IVerge>> {
|
||||
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);
|
||||
|
||||
@@ -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<String>
|
||||
}
|
||||
|
||||
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<String>
|
||||
|
||||
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<String>
|
||||
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<Stri
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> 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());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@ pub async fn create_local_backup() -> CmdResult<()> {
|
||||
|
||||
/// List local backups
|
||||
#[tauri::command]
|
||||
pub fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
feat::list_local_backup().stringify_err()
|
||||
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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]
|
||||
@@ -40,10 +43,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
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 {
|
||||
@@ -111,7 +111,7 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
10000u32
|
||||
}
|
||||
};
|
||||
@@ -128,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()?;
|
||||
@@ -151,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::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
@@ -231,7 +229,7 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("dns_config.yaml");
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
Ok(dns_path.exists())
|
||||
}
|
||||
@@ -244,7 +242,7 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
|
||||
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());
|
||||
@@ -257,10 +255,8 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
/// 验证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() {
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod runtime;
|
||||
pub mod save_profile;
|
||||
pub mod service;
|
||||
pub mod system;
|
||||
pub mod updater;
|
||||
pub mod uwp;
|
||||
pub mod validate;
|
||||
pub mod verge;
|
||||
@@ -34,6 +35,7 @@ pub use runtime::*;
|
||||
pub use save_profile::*;
|
||||
pub use service::*;
|
||||
pub use system::*;
|
||||
pub use updater::*;
|
||||
pub use uwp::*;
|
||||
pub use validate::*;
|
||||
pub use verge::*;
|
||||
|
||||
@@ -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<Mapping> {
|
||||
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<Mapping> {
|
||||
);
|
||||
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<Mapping> {
|
||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
@@ -41,7 +49,13 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,68 +15,19 @@ use crate::{
|
||||
ret_err,
|
||||
utils::{dirs, help, logging::Type},
|
||||
};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
// 全局请求序列号跟踪,用于避免队列化执行
|
||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
// 策略1: 尝试快速获取latest数据
|
||||
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
|
||||
let profiles = Config::profiles().await;
|
||||
let latest = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match latest_result {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, "快速获取配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2: 如果快速获取失败,尝试获取data()
|
||||
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
|
||||
let profiles = Config::profiles().await;
|
||||
let data = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match data_result {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, "获取draft配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Err(join_err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"获取draft配置任务失败或超时: {}",
|
||||
join_err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3: fallback,尝试重新创建配置
|
||||
logging!(warn, Type::Cmd, "所有获取配置策略都失败,尝试fallback");
|
||||
|
||||
Ok(IProfiles::new().await)
|
||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
||||
let draft = Config::profiles().await;
|
||||
let latest = draft.latest_ref();
|
||||
Ok((**latest).clone())
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
@@ -85,7 +36,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 +50,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
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 +61,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
}
|
||||
};
|
||||
|
||||
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 +96,13 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
/// 调整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 +112,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<String>) -> 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 +131,10 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
||||
/// 更新配置文件
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> 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 +145,7 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> 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 +157,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());
|
||||
}
|
||||
}
|
||||
@@ -334,65 +283,39 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
.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!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
logging!(info, Type::Cmd, "成功恢复到之前的配置");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_success(current_sequence: u64, current_value: Option<String>) -> CmdResult<bool> {
|
||||
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
|
||||
);
|
||||
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -406,53 +329,31 @@ async fn handle_validation_failure(
|
||||
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: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
|
||||
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<String>, current_sequence: u64) -> CmdResult<bool> {
|
||||
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
||||
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<String>,
|
||||
current_profile: Option<String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
CoreManager::global().update_config(),
|
||||
@@ -460,46 +361,36 @@ async fn perform_config_update(
|
||||
.await;
|
||||
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
|
||||
Ok(Ok((true, _))) => handle_success(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,
|
||||
Ok(Err(e)) => handle_update_error(e).await,
|
||||
Err(_) => handle_timeout(current_profile).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
|
||||
if CURRENT_SWITCHING_PROFILE
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(false);
|
||||
return Err("switch_in_progress".into());
|
||||
}
|
||||
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
|
||||
|
||||
// 为当前请求分配序列号
|
||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let target_profile = profiles.current.clone();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||
current_sequence,
|
||||
"开始修改配置文件,目标profile: {:?}",
|
||||
target_profile
|
||||
);
|
||||
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().await.latest_ref().current.clone();
|
||||
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
|
||||
@@ -509,50 +400,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
&& current_profile.as_ref() != Some(new_profile)
|
||||
&& validate_new_profile(new_profile).await.is_err()
|
||||
{
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"正在更新配置草稿,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
|
||||
let _ = Config::profiles().await.draft_mut().patch_config(&profiles);
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
|
||||
|
||||
// 在调用内核前再次验证请求有效性
|
||||
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);
|
||||
}
|
||||
|
||||
perform_config_update(current_sequence, current_value, current_profile).await
|
||||
perform_config_update(current_value, current_profile).await
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
@@ -572,33 +426,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -631,10 +486,15 @@ pub async fn view_profile(index: String) -> CmdResult {
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,28 +12,37 @@ use tokio::fs;
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> 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<String>) -> 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<String>) -> 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())
|
||||
}
|
||||
}
|
||||
|
||||
149
src-tauri/src/cmd/updater.rs
Normal file
149
src-tauri/src/cmd/updater.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use serde::Serialize;
|
||||
use tauri::{Manager, ResourceId, Runtime, webview::Webview};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use url::Url;
|
||||
|
||||
use super::{CmdResult, String};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateMetadata {
|
||||
rid: ResourceId,
|
||||
current_version: String,
|
||||
version: String,
|
||||
date: Option<String>,
|
||||
body: Option<String>,
|
||||
raw_json: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum UpdateChannel {
|
||||
Stable,
|
||||
Autobuild,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for UpdateChannel {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"stable" => Ok(Self::Stable),
|
||||
"autobuild" => Ok(Self::Autobuild),
|
||||
other => Err(String::from(format!("Unsupported channel \"{other}\""))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CHANNEL_RELEASE_TAGS: &[(UpdateChannel, &str)] = &[
|
||||
(UpdateChannel::Stable, "updater"),
|
||||
(UpdateChannel::Autobuild, "updater-autobuild"),
|
||||
];
|
||||
|
||||
const CHANNEL_ENDPOINT_TEMPLATES: &[&str] = &[
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
||||
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update.json",
|
||||
];
|
||||
|
||||
fn resolve_release_tag(channel: UpdateChannel) -> CmdResult<&'static str> {
|
||||
CHANNEL_RELEASE_TAGS
|
||||
.iter()
|
||||
.find_map(|(entry_channel, tag)| (*entry_channel == channel).then_some(*tag))
|
||||
.ok_or_else(|| {
|
||||
String::from(format!(
|
||||
"No release tag registered for update channel \"{channel:?}\""
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_channel_endpoints(channel: UpdateChannel) -> CmdResult<Vec<Url>> {
|
||||
let release_tag = resolve_release_tag(channel)?;
|
||||
CHANNEL_ENDPOINT_TEMPLATES
|
||||
.iter()
|
||||
.map(|template| {
|
||||
let endpoint = template.replace("{release}", release_tag);
|
||||
Url::parse(&endpoint).map_err(|err| {
|
||||
String::from(format!(
|
||||
"Failed to parse updater endpoint \"{endpoint}\": {err}"
|
||||
))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub async fn check_update_channel<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
channel: String,
|
||||
headers: Option<Vec<(String, String)>>,
|
||||
timeout: Option<u64>,
|
||||
proxy: Option<String>,
|
||||
target: Option<String>,
|
||||
allow_downgrades: Option<bool>,
|
||||
) -> CmdResult<Option<UpdateMetadata>> {
|
||||
let channel_enum = UpdateChannel::try_from(channel.as_str())?;
|
||||
let endpoints = resolve_channel_endpoints(channel_enum)?;
|
||||
|
||||
let mut builder = webview
|
||||
.updater_builder()
|
||||
.endpoints(endpoints)
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
|
||||
if let Some(headers) = headers {
|
||||
for (key, value) in headers {
|
||||
builder = builder
|
||||
.header(key.as_str(), value.as_str())
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
builder = builder.timeout(std::time::Duration::from_millis(timeout));
|
||||
}
|
||||
|
||||
if let Some(proxy) = proxy {
|
||||
let proxy_url = Url::parse(&proxy)
|
||||
.map_err(|err| String::from(format!("Invalid proxy URL \"{proxy}\": {err}")))?;
|
||||
builder = builder.proxy(proxy_url);
|
||||
}
|
||||
|
||||
if let Some(target) = target {
|
||||
builder = builder.target(target);
|
||||
}
|
||||
|
||||
let allow_downgrades = allow_downgrades.unwrap_or(channel_enum != UpdateChannel::Stable);
|
||||
|
||||
if allow_downgrades {
|
||||
builder = builder.version_comparator(|current, update| update.version != current);
|
||||
}
|
||||
|
||||
let updater = builder
|
||||
.build()
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
|
||||
let update = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|err| String::from(err.to_string()))?;
|
||||
|
||||
let Some(update) = update else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let formatted_date = update
|
||||
.date
|
||||
.as_ref()
|
||||
.map(|date| String::from(date.to_string()));
|
||||
|
||||
let metadata = UpdateMetadata {
|
||||
rid: webview.resources_table().add(update.clone()),
|
||||
current_version: String::from(update.current_version.clone()),
|
||||
version: String::from(update.version.clone()),
|
||||
date: formatted_date,
|
||||
body: update.body.clone().map(Into::into),
|
||||
raw_json: update.raw_json.clone(),
|
||||
};
|
||||
|
||||
Ok(Some(metadata))
|
||||
}
|
||||
@@ -9,12 +9,12 @@ pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
// 分离数据获取和异步调用
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -55,6 +56,20 @@ impl Config {
|
||||
pub async fn init_config() -> Result<()> {
|
||||
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 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);
|
||||
}
|
||||
|
||||
let validation_result = Self::generate_and_validate().await?;
|
||||
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
@@ -68,13 +83,13 @@ impl Config {
|
||||
// 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".into()).is_err() {
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".into()))?;
|
||||
profiles_append_item_safe(merge_item.clone()).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".into()).is_err() {
|
||||
let script_item = PrfItem::from_script(Some("Script".into()))?;
|
||||
profiles_append_item_safe(script_item.clone()).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(())
|
||||
}
|
||||
@@ -153,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(())
|
||||
}
|
||||
|
||||
@@ -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<Self>, other: Option<Self>) -> Option<Self> {
|
||||
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
|
||||
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<String>) -> Result<PrfItem> {
|
||||
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
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<String>,
|
||||
option: Option<PrfOption>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
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<String>,
|
||||
desc: Option<String>,
|
||||
option: Option<PrfOption>,
|
||||
name: Option<&String>,
|
||||
desc: Option<&String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
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<String> {
|
||||
pub async fn read_file(&self) -> Result<String> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +32,7 @@ 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -67,24 +68,17 @@ impl IProfiles {
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::default()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
@@ -95,17 +89,17 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
/// 只修改current,valid和chain
|
||||
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
|
||||
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
if let Some(current) = patch.current
|
||||
if let Some(current) = &patch.current
|
||||
&& let Some(items) = self.items.as_ref()
|
||||
{
|
||||
let some_uid = Some(current);
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
if items.iter().any(|e| e.uid.as_ref() == some_uid) {
|
||||
self.current = some_uid.cloned();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,28 +116,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<str>) -> 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
|
||||
@@ -165,7 +161,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() {
|
||||
@@ -173,24 +169,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);
|
||||
}
|
||||
}
|
||||
@@ -206,11 +201,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);
|
||||
@@ -232,13 +227,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());
|
||||
@@ -247,8 +242,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() {
|
||||
@@ -279,10 +274,10 @@ impl IProfiles {
|
||||
|
||||
/// delete item
|
||||
/// if delete the current then return true
|
||||
pub async fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(&uid);
|
||||
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
|
||||
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());
|
||||
@@ -330,7 +325,7 @@ impl IProfiles {
|
||||
.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()) {
|
||||
@@ -342,7 +337,7 @@ impl IProfiles {
|
||||
|
||||
self.items = Some(items);
|
||||
self.save_file().await?;
|
||||
Ok(current == uid)
|
||||
Ok(current == *uid)
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅内容
|
||||
@@ -433,8 +428,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,名称)
|
||||
@@ -453,6 +448,18 @@ impl IProfiles {
|
||||
})
|
||||
}
|
||||
|
||||
/// 通过 uid 获取名称
|
||||
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> {
|
||||
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<CleanupResult> {
|
||||
let profiles_dir = dirs::app_profiles_dir()?;
|
||||
@@ -491,7 +498,7 @@ impl IProfiles {
|
||||
{
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -500,11 +507,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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,8 +528,9 @@ impl IProfiles {
|
||||
failed_deletions,
|
||||
};
|
||||
|
||||
log::info!(
|
||||
target: "app",
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
||||
result.total_files,
|
||||
result.deleted_files.len(),
|
||||
@@ -626,14 +638,14 @@ impl IProfiles {
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn profiles_append_item_with_filedata_safe(
|
||||
item: PrfItem,
|
||||
item: &PrfItem,
|
||||
file_data: Option<String>,
|
||||
) -> 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 {
|
||||
@@ -643,7 +655,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 {
|
||||
@@ -653,7 +665,7 @@ pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
|
||||
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
@@ -663,7 +675,7 @@ pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
|
||||
.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 {
|
||||
@@ -683,7 +695,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 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
logging,
|
||||
@@ -304,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(())
|
||||
}
|
||||
|
||||
@@ -354,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()
|
||||
}
|
||||
}
|
||||
@@ -438,11 +437,11 @@ impl IVerge {
|
||||
/// patch verge config
|
||||
/// only save to file
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: IVerge) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -697,3 +696,9 @@ impl From<IVerge> for IVergeResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<IVerge>> for IVergeResponse {
|
||||
fn from(verge: Box<IVerge>) -> Self {
|
||||
IVergeResponse::from(*verge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
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 arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||
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
|
||||
@@ -51,24 +56,24 @@ impl Operation {
|
||||
}
|
||||
|
||||
pub struct WebDavClient {
|
||||
config: Arc<Mutex<Option<WebDavConfig>>>,
|
||||
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
|
||||
config: Arc<ArcSwapOption<WebDavConfig>>,
|
||||
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static WebDavClient {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||
config: Arc::new(Mutex::new(None)),
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
config: Arc::new(ArcSwapOption::new(None)),
|
||||
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
|
||||
// 先尝试从缓存获取
|
||||
{
|
||||
let clients = self.clients.lock();
|
||||
if let Some(client) = clients.get(&op) {
|
||||
let clients_map = self.clients.load();
|
||||
if let Some(client) = clients_map.get(&op) {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
}
|
||||
@@ -76,10 +81,10 @@ impl WebDavClient {
|
||||
// 获取或创建配置
|
||||
let config = {
|
||||
// 首先检查是否已有配置
|
||||
let existing_config = self.config.lock().as_ref().cloned();
|
||||
let existing_config = self.config.load();
|
||||
|
||||
if let Some(cfg) = existing_config {
|
||||
cfg
|
||||
if let Some(cfg_arc) = existing_config.clone() {
|
||||
(*cfg_arc).clone()
|
||||
} else {
|
||||
// 释放锁后获取异步配置
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
@@ -101,8 +106,8 @@ impl WebDavClient {
|
||||
password: verge.webdav_password.unwrap_or_default(),
|
||||
};
|
||||
|
||||
// 重新获取锁并存储配置
|
||||
*self.config.lock() = Some(config.clone());
|
||||
// 存储配置到 ArcSwapOption
|
||||
self.config.store(Some(Arc::new(config.clone())));
|
||||
config
|
||||
}
|
||||
};
|
||||
@@ -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!(
|
||||
@@ -151,18 +161,19 @@ impl WebDavClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存客户端
|
||||
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
|
||||
{
|
||||
let mut clients = self.clients.lock();
|
||||
clients.insert(op, client.clone());
|
||||
let mut map = (**self.clients.load()).clone();
|
||||
map.insert(op, client.clone());
|
||||
self.clients.store(map.into());
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
*self.config.lock() = None;
|
||||
self.clients.lock().clear();
|
||||
self.config.store(None);
|
||||
self.clients.store(Arc::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
||||
@@ -170,7 +181,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 +192,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 +207,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 +231,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 +269,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 +290,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 +308,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))
|
||||
}
|
||||
|
||||
@@ -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<ProxyEvent>,
|
||||
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
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<RwLock<ProxyState>>) {
|
||||
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<RwLock<ProxyState>>) {
|
||||
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<RwLock<ProxyState>>) {
|
||||
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<RwLock<ProxyState>>) {
|
||||
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<RwLock<ProxyState>>) {
|
||||
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");
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ use crate::{
|
||||
singleton_with_logging, utils::logging::Type,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use parking_lot::Mutex;
|
||||
use arc_swap::ArcSwap;
|
||||
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
|
||||
@@ -94,77 +93,64 @@ impl SystemHotkey {
|
||||
}
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>,
|
||||
current: ArcSwap<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Hotkey {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Vec::new())),
|
||||
current: ArcSwap::new(Arc::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,9 +272,9 @@ impl Hotkey {
|
||||
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
|
||||
|
||||
impl Hotkey {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
pub async fn init(&self, skip: bool) -> Result<()> {
|
||||
let verge = Config::verge().await;
|
||||
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||
let enable_global_hotkey = !skip && verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
@@ -299,10 +283,6 @@ impl Hotkey {
|
||||
enable_global_hotkey
|
||||
);
|
||||
|
||||
if !enable_global_hotkey {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Extract hotkeys data before async operations
|
||||
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
|
||||
|
||||
@@ -360,7 +340,7 @@ impl Hotkey {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current.lock().clone_from(&hotkeys);
|
||||
self.current.store(Arc::new(hotkeys));
|
||||
} else {
|
||||
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
||||
}
|
||||
@@ -391,8 +371,8 @@ impl Hotkey {
|
||||
|
||||
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||
// Extract current hotkeys before async operations
|
||||
let current_hotkeys = self.current.lock().clone();
|
||||
let old_map = Self::get_map_from_vec(¤t_hotkeys);
|
||||
let current_hotkeys = &*self.current.load();
|
||||
let old_map = Self::get_map_from_vec(current_hotkeys);
|
||||
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
||||
|
||||
let (del, add) = Self::get_diff(old_map, new_map);
|
||||
@@ -406,7 +386,7 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
// Update the current hotkeys after all async operations
|
||||
*self.current.lock() = new_hotkeys;
|
||||
self.current.store(Arc::new(new_hotkeys));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -39,25 +39,20 @@ impl CoreManager {
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
let _permit = self
|
||||
.update_semaphore
|
||||
.try_acquire()
|
||||
.map_err(|_| anyhow!("Config update already in progress"))?;
|
||||
|
||||
self.perform_config_update().await
|
||||
}
|
||||
|
||||
fn should_update_config(&self) -> Result<bool> {
|
||||
let now = Instant::now();
|
||||
let mut last = self.last_update.lock();
|
||||
let last = self.get_last_update();
|
||||
|
||||
if let Some(last_time) = *last
|
||||
&& now.duration_since(last_time) < timing::CONFIG_UPDATE_DEBOUNCE
|
||||
if let Some(last_time) = last
|
||||
&& now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
*last = Some(now);
|
||||
self.set_last_update(now);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{CoreManager, RunningMode};
|
||||
use crate::config::{Config, ConfigType, IVerge};
|
||||
use crate::{
|
||||
core::{
|
||||
logger::CLASH_LOGGER,
|
||||
@@ -41,18 +42,12 @@ impl CoreManager {
|
||||
self.start_core().await
|
||||
}
|
||||
|
||||
pub async fn change_core(&self, clash_core: Option<String>) -> 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();
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
mod config;
|
||||
mod lifecycle;
|
||||
mod process;
|
||||
mod state;
|
||||
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use std::{fmt, sync::Arc, time::Instant};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use crate::process::CommandChildGuard;
|
||||
use crate::singleton_lazy;
|
||||
@@ -30,22 +28,21 @@ impl fmt::Display for RunningMode {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
state: Arc<Mutex<State>>,
|
||||
update_semaphore: Arc<Semaphore>,
|
||||
last_update: Arc<Mutex<Option<Instant>>>,
|
||||
state: ArcSwap<State>,
|
||||
last_update: ArcSwapOption<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
running_mode: Arc<RunningMode>,
|
||||
child_sidecar: Option<CommandChildGuard>,
|
||||
running_mode: ArcSwap<RunningMode>,
|
||||
child_sidecar: ArcSwapOption<CommandChildGuard>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
running_mode: Arc::new(RunningMode::NotRunning),
|
||||
child_sidecar: None,
|
||||
running_mode: ArcSwap::new(Arc::new(RunningMode::NotRunning)),
|
||||
child_sidecar: ArcSwapOption::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,28 +50,44 @@ impl Default for State {
|
||||
impl Default for CoreManager {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(State::default())),
|
||||
update_semaphore: Arc::new(Semaphore::new(1)),
|
||||
last_update: Arc::new(Mutex::new(None)),
|
||||
state: ArcSwap::new(Arc::new(State::default())),
|
||||
last_update: ArcSwapOption::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn get_running_mode(&self) -> Arc<RunningMode> {
|
||||
Arc::clone(&self.state.lock().running_mode)
|
||||
Arc::clone(&self.state.load().running_mode.load())
|
||||
}
|
||||
|
||||
pub fn take_child_sidecar(&self) -> Option<CommandChildGuard> {
|
||||
self.state
|
||||
.load()
|
||||
.child_sidecar
|
||||
.swap(None)
|
||||
.and_then(|arc| Arc::try_unwrap(arc).ok())
|
||||
}
|
||||
|
||||
pub fn get_last_update(&self) -> Option<Arc<Instant>> {
|
||||
self.last_update.load_full()
|
||||
}
|
||||
|
||||
pub fn set_running_mode(&self, mode: RunningMode) {
|
||||
self.state.lock().running_mode = Arc::new(mode);
|
||||
let state = self.state.load();
|
||||
state.running_mode.store(Arc::new(mode));
|
||||
}
|
||||
|
||||
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
|
||||
self.state.lock().child_sidecar = Some(child);
|
||||
let state = self.state.load();
|
||||
state.child_sidecar.store(Some(Arc::new(child)));
|
||||
}
|
||||
|
||||
pub fn set_last_update(&self, time: Instant) {
|
||||
self.last_update.store(Some(Arc::new(time)));
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
self.cleanup_orphaned_processes().await?;
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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<u32>, 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<Vec<u32>> {
|
||||
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::<PROCESSENTRY32W>() 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<u32> = 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<bool> {
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,8 +93,7 @@ impl CoreManager {
|
||||
defer! {
|
||||
self.set_running_mode(RunningMode::NotRunning);
|
||||
}
|
||||
let mut state = self.state.lock();
|
||||
if let Some(child) = state.child_sidecar.take() {
|
||||
if let Some(child) = self.take_child_sidecar() {
|
||||
let pid = child.pid();
|
||||
drop(child);
|
||||
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::tray,
|
||||
logging, logging_error,
|
||||
utils::{dirs, init::service_writer_config, logging::Type},
|
||||
};
|
||||
@@ -531,6 +532,7 @@ impl ServiceManager {
|
||||
return Err(anyhow::anyhow!("服务不可用: {}", reason));
|
||||
}
|
||||
}
|
||||
let _ = tray::Tray::global().update_tray_display().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<std::string::String>) -> 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Box<dyn std::future::Future<Output = ()> + 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ define_menu! {
|
||||
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",
|
||||
|
||||
@@ -2,9 +2,11 @@ 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;
|
||||
@@ -25,7 +27,6 @@ use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -61,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
|
||||
}
|
||||
}
|
||||
@@ -85,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);
|
||||
}
|
||||
@@ -122,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);
|
||||
}
|
||||
@@ -159,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);
|
||||
}
|
||||
@@ -206,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(());
|
||||
}
|
||||
|
||||
@@ -214,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: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白
|
||||
@@ -229,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(());
|
||||
}
|
||||
|
||||
@@ -249,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(());
|
||||
}
|
||||
// 调整最小更新间隔,确保状态及时刷新
|
||||
@@ -297,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
|
||||
@@ -322,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(())
|
||||
}
|
||||
}
|
||||
@@ -341,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(());
|
||||
}
|
||||
|
||||
@@ -350,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(());
|
||||
}
|
||||
};
|
||||
@@ -380,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(());
|
||||
}
|
||||
|
||||
@@ -389,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(());
|
||||
}
|
||||
};
|
||||
@@ -412,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(());
|
||||
}
|
||||
|
||||
@@ -430,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(());
|
||||
}
|
||||
|
||||
@@ -486,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(())
|
||||
@@ -494,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?;
|
||||
@@ -507,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;
|
||||
@@ -557,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,
|
||||
@@ -592,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(());
|
||||
}
|
||||
|
||||
@@ -645,17 +672,15 @@ async fn create_profile_menu_item(
|
||||
.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
|
||||
.latest_ref()
|
||||
.is_current_profile_index(profile_uid.clone());
|
||||
.is_current_profile_index(profile_uid);
|
||||
CheckMenuItem::with_id(
|
||||
&app_handle,
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(&profile_name).await,
|
||||
t(profile_name).await,
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
@@ -726,7 +751,9 @@ fn create_subcreate_proxy_menu_item(
|
||||
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();
|
||||
@@ -768,7 +795,12 @@ fn create_subcreate_proxy_menu_item(
|
||||
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,6 +869,7 @@ async fn create_tray_menu(
|
||||
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<tauri::menu::Menu<Wry>> {
|
||||
@@ -980,7 +1013,7 @@ async fn create_tray_menu(
|
||||
app_handle,
|
||||
MenuIds::TUN_MODE,
|
||||
&texts.tun_mode,
|
||||
true,
|
||||
tun_mode_available,
|
||||
tun_mode_enabled,
|
||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||
)?;
|
||||
@@ -1034,12 +1067,34 @@ async fn create_tray_menu(
|
||||
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,
|
||||
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(
|
||||
@@ -1138,7 +1193,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
feat::change_clash_mode(mode.into()).await;
|
||||
}
|
||||
MenuIds::DASHBOARD => {
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
logging!(info, Type::Tray, "托盘菜单点击: 打开窗口");
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
@@ -1155,7 +1210,11 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
MenuIds::COPY_ENV => feat::copy_clash_env().await,
|
||||
@@ -1169,6 +1228,12 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
MenuIds::LOGS_DIR => {
|
||||
let _ = cmd::open_logs_dir().await;
|
||||
}
|
||||
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 => {
|
||||
@@ -1202,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()
|
||||
@@ -1216,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", ());
|
||||
@@ -1230,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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::config::{Config, ConfigType};
|
||||
use crate::core::handle;
|
||||
@@ -33,19 +33,16 @@ impl CoreConfigValidator {
|
||||
|
||||
impl CoreConfigValidator {
|
||||
/// 检查文件是否为脚本文件
|
||||
fn is_script_file<P>(path: P) -> Result<bool>
|
||||
where
|
||||
P: AsRef<Path> + std::fmt::Display,
|
||||
{
|
||||
async fn is_script_file(path: &str) -> Result<bool> {
|
||||
// 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!(
|
||||
@@ -115,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();
|
||||
@@ -144,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();
|
||||
@@ -216,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内核验证
|
||||
@@ -246,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内核验证
|
||||
|
||||
@@ -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<ChainItem> {
|
||||
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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@ 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)]
|
||||
@@ -136,7 +140,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&merge_uid).ok().cloned()
|
||||
profiles.get_item(merge_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -153,7 +157,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&script_uid).ok().cloned()
|
||||
profiles.get_item(script_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -170,7 +174,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&rules_uid).ok().cloned()
|
||||
profiles.get_item(rules_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -187,7 +191,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&proxies_uid).ok().cloned()
|
||||
profiles.get_item(proxies_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -204,7 +208,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&groups_uid).ok().cloned()
|
||||
profiles.get_item(groups_uid).ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -221,7 +225,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&"Merge".into()).ok().cloned()
|
||||
profiles.get_item("Merge").ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -238,7 +242,7 @@ async fn collect_profile_items() -> ProfileItems {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
profiles.get_item(&"Script".into()).ok().cloned()
|
||||
profiles.get_item("Script").ok().cloned()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
<Option<ChainItem>>::from_async(&item).await
|
||||
@@ -420,14 +424,14 @@ fn apply_builtin_scripts(
|
||||
.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}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,34 +441,29 @@ fn apply_builtin_scripts(
|
||||
config
|
||||
}
|
||||
|
||||
fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
|
||||
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::<serde_yaml_ng::Mapping>(&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::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
{
|
||||
if let Some(hosts_value) = dns_config.get("hosts")
|
||||
&& hosts_value.is_mapping()
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,7 +539,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
config = use_sort(config);
|
||||
|
||||
// dns settings
|
||||
config = apply_dns_settings(config, enable_dns_settings);
|
||||
config = apply_dns_settings(config, enable_dns_settings).await;
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<LocalBackupFile>> {
|
||||
pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
|
||||
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::<Utc>::from(time).to_rfc3339())
|
||||
@@ -233,18 +241,23 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
||||
return Err(anyhow!("Backup file not found: {}", filename));
|
||||
}
|
||||
|
||||
let verge = Config::verge().await;
|
||||
let verge_data = verge.latest_ref().clone();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.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 +271,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 +280,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(())
|
||||
|
||||
@@ -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<u32> {
|
||||
|
||||
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<u32> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {err:#?}");
|
||||
logging!(trace, Type::Network, "test_delay error: {err:#?}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,15 +226,12 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(patch.clone());
|
||||
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 update_flags = determine_update_flags(patch);
|
||||
let process_flag_result: std::result::Result<(), anyhow::Error> = {
|
||||
process_terminated_flags(update_flags, &patch).await?;
|
||||
process_terminated_flags(update_flags, patch).await?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
@@ -245,7 +242,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge().await.apply();
|
||||
if !not_save_file {
|
||||
// 分离数据获取和异步调用
|
||||
let verge_data = Config::verge().await.data_mut().clone();
|
||||
let verge_data = Config::verge().await.data_ref().clone();
|
||||
verge_data.save_file().await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -18,36 +18,57 @@ pub async fn toggle_proxy_profile(profile_index: String) {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Tray, "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_update_profile(uid: String) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
async fn should_update_profile(
|
||||
uid: &String,
|
||||
ignore_auto_update: bool,
|
||||
) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
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} 不是远程订阅,跳过更新");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] {uid} 不是远程订阅,跳过更新"
|
||||
);
|
||||
Ok(None)
|
||||
} else if item.url.is_none() {
|
||||
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: [订阅更新] {uid} 缺少URL,无法更新"
|
||||
);
|
||||
bail!("failed to get the profile item url");
|
||||
} else if !item
|
||||
.option
|
||||
.as_ref()
|
||||
.and_then(|o| o.allow_auto_update)
|
||||
.unwrap_or(true)
|
||||
} else if !ignore_auto_update
|
||||
&& !item
|
||||
.option
|
||||
.as_ref()
|
||||
.and_then(|o| o.allow_auto_update)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
log::info!(target: "app", "[订阅更新] {} 禁止自动更新,跳过更新", uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] {} 禁止自动更新,跳过更新",
|
||||
uid
|
||||
);
|
||||
Ok(None)
|
||||
} else {
|
||||
log::info!(target: "app",
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
||||
item.url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
||||
);
|
||||
Ok(Some((
|
||||
item.url
|
||||
@@ -59,79 +80,111 @@ async fn should_update_profile(uid: String) -> Result<Option<(String, Option<Prf
|
||||
}
|
||||
|
||||
async fn perform_profile_update(
|
||||
uid: String,
|
||||
url: String,
|
||||
opt: Option<PrfOption>,
|
||||
option: Option<PrfOption>,
|
||||
uid: &String,
|
||||
url: &String,
|
||||
opt: Option<&PrfOption>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<bool> {
|
||||
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||
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;
|
||||
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
let profiles = Config::profiles().await;
|
||||
profiles_draft_update_item_safe(uid.clone(), item).await?;
|
||||
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
Ok(is_current)
|
||||
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) => {
|
||||
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);
|
||||
|
||||
let mut fallback_opt = merged_opt.unwrap_or_default();
|
||||
fallback_opt.with_proxy = Some(false);
|
||||
fallback_opt.self_proxy = Some(true);
|
||||
|
||||
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||
Ok(mut item) => {
|
||||
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||
|
||||
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;
|
||||
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}");
|
||||
Ok(is_current)
|
||||
}
|
||||
Err(retry_err) => {
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{retry_err}"),
|
||||
);
|
||||
Err(retry_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: Option<bool>,
|
||||
uid: &String,
|
||||
option: Option<&PrfOption>,
|
||||
auto_refresh: bool,
|
||||
ignore_auto_update: bool,
|
||||
) -> Result<()> {
|
||||
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
let auto_refresh = auto_refresh.unwrap_or(true);
|
||||
|
||||
let url_opt = should_update_profile(uid.clone()).await?;
|
||||
let url_opt = should_update_profile(uid, ignore_auto_update).await?;
|
||||
|
||||
let should_refresh = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
perform_profile_update(uid.clone(), url, opt, option).await? && auto_refresh
|
||||
perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh
|
||||
}
|
||||
None => auto_refresh,
|
||||
};
|
||||
@@ -146,7 +199,7 @@ pub async fn update_profile(
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle,
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
@@ -16,11 +18,15 @@ pub async fn toggle_system_proxy() {
|
||||
&& 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()
|
||||
},
|
||||
@@ -30,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}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +46,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
&IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
@@ -49,7 +55,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AppHandle> = 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<dyn std::error::Error>> {
|
||||
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,
|
||||
@@ -171,6 +174,7 @@ mod app_init {
|
||||
cmd::get_runtime_logs,
|
||||
cmd::get_runtime_proxy_chain_config,
|
||||
cmd::update_proxy_chain_config_in_runtime,
|
||||
cmd::check_update_channel,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::sync_tray_proxy_selection,
|
||||
@@ -285,6 +289,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;
|
||||
@@ -326,10 +335,7 @@ pub fn run() {
|
||||
.register_system_hotkey(SystemHotkey::CmdW)
|
||||
.await;
|
||||
}
|
||||
|
||||
if !is_enable_global_hotkey {
|
||||
let _ = hotkey::Hotkey::global().init().await;
|
||||
}
|
||||
let _ = hotkey::Hotkey::global().init(true).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,8 +356,18 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
|
||||
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
let _ = hotkey::Hotkey::global().reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,5 @@ fn main() {
|
||||
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
|
||||
}
|
||||
}
|
||||
|
||||
app_lib::run();
|
||||
}
|
||||
|
||||
@@ -43,49 +43,48 @@ 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}");
|
||||
async fn refresh_lightweight_tray_state() {
|
||||
if let Err(err) = Tray::global().update_tray_display().await {
|
||||
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
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 +95,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,60 +134,33 @@ 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, "无需进入轻量模式,跳过调用");
|
||||
refresh_lightweight_tray_state().await;
|
||||
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);
|
||||
|
||||
refresh_lightweight_tray_state().await;
|
||||
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,
|
||||
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
|
||||
);
|
||||
refresh_lightweight_tray_state().await;
|
||||
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);
|
||||
refresh_lightweight_tray_state().await;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -215,24 +171,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 +203,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 +215,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 +264,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 +271,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 {
|
||||
|
||||
@@ -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<CommandChild>);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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<bool> = 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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
Ok(app_home_dir()?.join("logs"))
|
||||
}
|
||||
|
||||
// latest verge log
|
||||
pub fn app_latest_log() -> Result<PathBuf> {
|
||||
Ok(app_logs_dir()?.join("latest.log"))
|
||||
}
|
||||
|
||||
/// local backups dir
|
||||
pub fn local_backup_dir() -> Result<PathBuf> {
|
||||
let dir = app_home_dir()?.join(BACKUP_DIR);
|
||||
@@ -167,6 +183,15 @@ pub fn service_log_dir() -> Result<PathBuf> {
|
||||
Ok(log_dir)
|
||||
}
|
||||
|
||||
pub fn clash_latest_log() -> Result<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -24,22 +24,21 @@ impl<T: Clone + ToOwned> From<T> for Draft<T> {
|
||||
///
|
||||
/// # 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<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
/// 正式数据视图
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 可写正式数据
|
||||
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
|
||||
}
|
||||
|
||||
/// 返回正式数据的只读视图(不包含草稿)
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 创建或获取草稿并返回可写引用
|
||||
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
let guard = self.inner.upgradable_read();
|
||||
@@ -69,17 +68,21 @@ impl<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
}
|
||||
|
||||
/// 提交草稿,返回旧正式数据
|
||||
pub fn apply(&self) -> Option<Box<T>> {
|
||||
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<Box<T>> {
|
||||
self.inner.write().1.take()
|
||||
pub fn discard(&self) {
|
||||
self.inner.write().1.take();
|
||||
}
|
||||
|
||||
/// 异步修改正式数据,闭包直接获得 Box<T> 所有权
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
@@ -364,7 +365,7 @@ async fn initialize_config_files() -> Result<()> {
|
||||
if let Ok(path) = dirs::profiles_path()
|
||||
&& !path.exists()
|
||||
{
|
||||
let template = IProfiles::template();
|
||||
let template = IProfiles::default();
|
||||
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -125,15 +120,11 @@ pub(super) async fn init_timer() {
|
||||
}
|
||||
|
||||
pub(super) async fn init_hotkey() {
|
||||
logging_error!(Type::Setup, Hotkey::global().init().await);
|
||||
logging_error!(Type::Setup, Hotkey::global().init(false).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() {
|
||||
|
||||
@@ -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<String> = 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: {:?}",
|
||||
|
||||
@@ -21,16 +21,14 @@ const MINIMAL_HEIGHT: f64 = 520.0;
|
||||
pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
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()
|
||||
|
||||
@@ -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::<QueryParam>())
|
||||
.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::<std::string::String>(
|
||||
"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()
|
||||
{
|
||||
|
||||
@@ -58,7 +58,11 @@ fn get_window_operation_debounce() -> &'static Mutex<Instant> {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -506,7 +506,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
// 导航到代理页面
|
||||
const goToProxies = useCallback(() => {
|
||||
navigate("/");
|
||||
navigate("/proxies");
|
||||
}, [navigate]);
|
||||
|
||||
// 获取要显示的代理节点
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useServiceInstaller } from "@/hooks/useServiceInstaller";
|
||||
import { getSystemInfo } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
@@ -59,6 +60,7 @@ export const SystemInfoCard = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isAdminMode, isSidecarMode } = useSystemState();
|
||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
// 系统信息状态
|
||||
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
||||
@@ -117,7 +119,7 @@ export const SystemInfoCard = () => {
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
if (verge?.auto_check_update) {
|
||||
checkUpdate().catch(console.error);
|
||||
checkUpdate(updateChannel).catch(console.error);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
@@ -126,11 +128,11 @@ export const SystemInfoCard = () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [verge?.auto_check_update, dispatchSystemState]);
|
||||
}, [verge?.auto_check_update, dispatchSystemState, updateChannel]);
|
||||
|
||||
// 自动检查更新逻辑
|
||||
useSWR(
|
||||
verge?.auto_check_update ? "checkUpdate" : null,
|
||||
verge?.auto_check_update ? ["checkUpdate", updateChannel] : null,
|
||||
async () => {
|
||||
const now = Date.now();
|
||||
localStorage.setItem("last_check_update", now.toString());
|
||||
@@ -138,7 +140,7 @@ export const SystemInfoCard = () => {
|
||||
type: "set-last-check-update",
|
||||
payload: new Date(now).toLocaleString(),
|
||||
});
|
||||
return await checkUpdate();
|
||||
return await checkUpdate(updateChannel);
|
||||
},
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
@@ -172,7 +174,7 @@ export const SystemInfoCard = () => {
|
||||
// 检查更新
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
const info = await checkUpdate(updateChannel);
|
||||
if (!info?.available) {
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import useSWR from "swr";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { checkUpdateSafe } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
|
||||
import { DialogRef } from "../base";
|
||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||
@@ -16,12 +17,14 @@ export const UpdateButton = (props: Props) => {
|
||||
const { className } = props;
|
||||
const { verge } = useVerge();
|
||||
const { auto_check_update } = verge || {};
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
const viewerRef = useRef<DialogRef>(null);
|
||||
|
||||
const shouldCheck = auto_check_update || auto_check_update === null;
|
||||
const { data: updateInfo } = useSWR(
|
||||
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
|
||||
checkUpdateSafe,
|
||||
shouldCheck ? ["checkUpdate", updateChannel] : null,
|
||||
() => checkUpdateSafe(updateChannel),
|
||||
{
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -15,6 +15,7 @@ import { portableFlag } from "@/pages/_layout";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useSetUpdateState, useUpdateState } from "@/services/states";
|
||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
|
||||
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,12 +27,17 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const updateState = useUpdateState();
|
||||
const setUpdateState = useSetUpdateState();
|
||||
const { addListener } = useListen();
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
});
|
||||
const { data: updateInfo } = useSWR(
|
||||
["checkUpdate", updateChannel],
|
||||
() => checkUpdate(updateChannel),
|
||||
{
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
const [downloaded, setDownloaded] = useState(0);
|
||||
const [buffer, setBuffer] = useState(0);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||
import { useUpdateChannel } from "@/services/updateChannel";
|
||||
import { version } from "@root/package.json";
|
||||
|
||||
import { BackupViewer } from "./mods/backup-viewer";
|
||||
@@ -42,10 +43,11 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
const liteModeRef = useRef<DialogRef>(null);
|
||||
const [updateChannel] = useUpdateChannel();
|
||||
|
||||
const onCheckUpdate = async () => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
const info = await checkUpdate(updateChannel);
|
||||
if (!info?.available) {
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ContentCopyRounded } from "@mui/icons-material";
|
||||
import { Button, Input, MenuItem, Select } from "@mui/material";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
} from "@mui/material";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -11,6 +17,11 @@ import { navItems } from "@/pages/_routers";
|
||||
import { copyClashEnv } from "@/services/cmds";
|
||||
import { supportedLanguages } from "@/services/i18n";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import {
|
||||
UPDATE_CHANNEL_OPTIONS,
|
||||
type UpdateChannel,
|
||||
useUpdateChannel,
|
||||
} from "@/services/updateChannel";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { BackupViewer } from "./mods/backup-viewer";
|
||||
@@ -69,6 +80,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
const layoutRef = useRef<DialogRef>(null);
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
const [updateChannel, setUpdateChannel] = useUpdateChannel();
|
||||
|
||||
const onChangeData = (patch: any) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
@@ -79,6 +91,14 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
showNotice("success", t("Copy Success"), 1000);
|
||||
}, [t]);
|
||||
|
||||
const onUpdateChannelChange = useCallback(
|
||||
(event: SelectChangeEvent<UpdateChannel>) => {
|
||||
const nextChannel = event.target.value as UpdateChannel;
|
||||
setUpdateChannel(nextChannel);
|
||||
},
|
||||
[setUpdateChannel],
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingList title={t("Verge Basic Setting")}>
|
||||
<ThemeViewer ref={themeRef} />
|
||||
@@ -89,6 +109,21 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
<UpdateViewer ref={updateRef} />
|
||||
<BackupViewer ref={backupRef} />
|
||||
|
||||
<SettingItem label={t("Update Channel")}>
|
||||
<Select
|
||||
size="small"
|
||||
value={updateChannel}
|
||||
onChange={onUpdateChannelChange}
|
||||
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
||||
>
|
||||
{UPDATE_CHANNEL_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{t(option.labelKey)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
value={language ?? "en"}
|
||||
|
||||
@@ -117,13 +117,8 @@ const ProxyControlSwitches = ({
|
||||
const { uninstallServiceAndRestartCore } = useServiceUninstaller();
|
||||
const { actualState: systemProxyActualState, toggleSystemProxy } =
|
||||
useSystemProxyState();
|
||||
const {
|
||||
isServiceMode,
|
||||
isTunModeAvailable,
|
||||
mutateRunningMode,
|
||||
mutateServiceOk,
|
||||
mutateTunModeAvailable,
|
||||
} = useSystemState();
|
||||
const { isServiceOk, isTunModeAvailable, mutateSystemState } =
|
||||
useSystemState();
|
||||
|
||||
const sysproxyRef = useRef<DialogRef>(null);
|
||||
const tunRef = useRef<DialogRef>(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 && (
|
||||
<TooltipIcon
|
||||
title={t("TUN requires Service Mode or Admin Mode")}
|
||||
icon={WarningRounded}
|
||||
sx={{ color: "warning.main", ml: 1 }}
|
||||
/>
|
||||
<>
|
||||
<TooltipIcon
|
||||
title={t("TUN requires Service Mode or Admin Mode")}
|
||||
icon={WarningRounded}
|
||||
sx={{ color: "warning.main", ml: 1 }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t("Install Service")}
|
||||
icon={BuildRounded}
|
||||
color="primary"
|
||||
onClick={onInstallService}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isTunModeAvailable && (
|
||||
<TooltipIcon
|
||||
title={t("Install Service")}
|
||||
icon={BuildRounded}
|
||||
color="primary"
|
||||
onClick={onInstallService}
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
{isServiceMode && (
|
||||
{isServiceOk && (
|
||||
<TooltipIcon
|
||||
title={t("Uninstall Service")}
|
||||
icon={DeleteForeverRounded}
|
||||
|
||||
@@ -62,7 +62,9 @@ export const useProfiles = () => {
|
||||
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||
if (profiles?.current) {
|
||||
await patchProfile(profiles.current, value);
|
||||
mutateProfiles();
|
||||
if (!value.selected) {
|
||||
mutateProfiles();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"Label-Connections": "الاتصالات",
|
||||
"Label-Rules": "القواعد",
|
||||
"Label-Logs": "السجلات",
|
||||
"Label-Test": "اختبار",
|
||||
"Label-Settings": "الإعدادات",
|
||||
"Proxies": "الوكلاء",
|
||||
"Proxy Groups": "مجموعات الوكلاء",
|
||||
@@ -44,6 +43,9 @@
|
||||
"Proxy detail": "تفاصيل الوكيل",
|
||||
"Profiles": "الملفات الشخصية",
|
||||
"Update All Profiles": "تحديث جميع الملفات الشخصية",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "عرض تكوين وقت التشغيل",
|
||||
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
|
||||
"Paste": "لصق",
|
||||
@@ -166,7 +168,6 @@
|
||||
"Table View": "عرض الجدول",
|
||||
"List View": "عرض القائمة",
|
||||
"Close All": "إغلاق الكل",
|
||||
"Default": "افتراضي",
|
||||
"Download Speed": "سرعة التنزيل",
|
||||
"Upload Speed": "سرعة الرفع",
|
||||
"Host": "المضيف",
|
||||
@@ -197,6 +198,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 +282,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 +369,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 +412,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 +427,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 +453,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"
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"Proxy detail": "Knotendetails anzeigen",
|
||||
"Profiles": "Abonnement",
|
||||
"Update All Profiles": "Alle Abonnements aktualisieren",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
|
||||
"Reactivate Profiles": "Abonnement erneut aktivieren",
|
||||
"Paste": "Einfügen",
|
||||
@@ -195,10 +198,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 +258,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 +372,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 +527,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 +558,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"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
"Proxy detail": "Proxy detail",
|
||||
"Profiles": "Profiles",
|
||||
"Update All Profiles": "Update All Profiles",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "View Runtime Config",
|
||||
"Reactivate Profiles": "Reactivate Profiles",
|
||||
"Paste": "Paste",
|
||||
@@ -628,7 +631,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 +715,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"
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"Proxy detail": "Mostrar detalles del nodo",
|
||||
"Profiles": "Suscripciones",
|
||||
"Update All Profiles": "Actualizar todas las suscripciones",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Ver configuración en tiempo de ejecución",
|
||||
"Reactivate Profiles": "Reactivar suscripciones",
|
||||
"Paste": "Pegar",
|
||||
@@ -195,10 +198,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 +258,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 +372,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 +527,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 +558,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"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"Label-Connections": "اتصالات",
|
||||
"Label-Rules": "قوانین",
|
||||
"Label-Logs": "لاگها",
|
||||
"Label-Test": "آزمون",
|
||||
"Label-Settings": "تنظیمات",
|
||||
"Proxies": "پراکسیها",
|
||||
"Proxy Groups": "گروههای پراکسی",
|
||||
@@ -44,6 +43,9 @@
|
||||
"Proxy detail": "جزئیات پراکسی",
|
||||
"Profiles": "پروفایلها",
|
||||
"Update All Profiles": "بهروزرسانی همه پروفایلها",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
|
||||
"Reactivate Profiles": "فعالسازی مجدد پروفایلها",
|
||||
"Paste": "چسباندن",
|
||||
@@ -166,7 +168,6 @@
|
||||
"Table View": "نمای جدولی",
|
||||
"List View": "نمای لیستی",
|
||||
"Close All": "بستن همه",
|
||||
"Default": "پیشفرض",
|
||||
"Download Speed": "سرعت دانلود",
|
||||
"Upload Speed": "سرعت بارگذاری",
|
||||
"Host": "میزبان",
|
||||
@@ -197,6 +198,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 +278,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 +371,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 +455,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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -76,6 +43,9 @@
|
||||
"Proxy detail": "Detail Proksi",
|
||||
"Profiles": "Profil",
|
||||
"Update All Profiles": "Perbarui Semua Profil",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Lihat Konfigurasi Runtime",
|
||||
"Reactivate Profiles": "Reaktivasi Profil",
|
||||
"Paste": "Tempel",
|
||||
@@ -198,7 +168,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 +198,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 +283,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 +371,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 +410,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 +455,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"
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"Proxy detail": "ノードの詳細を表示する",
|
||||
"Profiles": "プロファイル",
|
||||
"Update All Profiles": "すべてのプロファイルを更新",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "実行時のプロファイルを表示",
|
||||
"Reactivate Profiles": "プロファイルを再アクティブ化",
|
||||
"Paste": "貼り付け",
|
||||
@@ -195,10 +198,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 +258,7 @@
|
||||
"Unified Delay Info": "統一遅延を有効にすると、2回の遅延テストが行われ、接続ハンドシェイクなどによる異なるタイプのノードの遅延差を解消します。",
|
||||
"Log Level": "ログレベル",
|
||||
"Log Level Info": "ログディレクトリのServiceフォルダ内のコアログファイルにのみ適用されます。",
|
||||
"Port Config": "ポート設定",
|
||||
"Random Port": "ランダムポート",
|
||||
"Mixed Port": "混合プロキシポート",
|
||||
"Socks Port": "SOCKSプロキシポート",
|
||||
@@ -370,17 +375,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 +530,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 +561,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 +571,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 ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
"Proxy detail": "프록시 상세",
|
||||
"Profiles": "프로필",
|
||||
"Update All Profiles": "모든 프로필 업데이트",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "런타임 설정 보기",
|
||||
"Reactivate Profiles": "프로필 재활성화",
|
||||
"Paste": "붙여넣기",
|
||||
@@ -127,6 +130,7 @@
|
||||
"Lazy": "지연 로딩",
|
||||
"Timeout": "타임아웃",
|
||||
"Max Failed Times": "최대 실패 횟수",
|
||||
"Interface Name": "인터페이스 이름",
|
||||
"Routing Mark": "라우팅 마크",
|
||||
"Include All": "모든 프록시 및 제공자 포함",
|
||||
"Include All Providers": "모든 제공자 포함",
|
||||
@@ -198,176 +202,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 +257,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": "노란색"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"Proxy detail": "Отображать больше сведений о прокси",
|
||||
"Profiles": "Профили",
|
||||
"Update All Profiles": "Обновить все профили",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||
"Reactivate Profiles": "Перезапустить профиль",
|
||||
"Paste": "Вставить",
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
"Proxy detail": "Vekil detayı",
|
||||
"Profiles": "Profiller",
|
||||
"Update All Profiles": "Tüm Profilleri Güncelle",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
|
||||
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
|
||||
"Paste": "Yapıştır",
|
||||
@@ -508,7 +511,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 +577,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 +617,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"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"Label-Connections": "Тоташулар",
|
||||
"Label-Rules": "Кагыйдәләр",
|
||||
"Label-Logs": "Логлар",
|
||||
"Label-Test": "Тест",
|
||||
"Label-Settings": "Көйләүләр",
|
||||
"Proxies": "Прокси",
|
||||
"Proxy Groups": "Прокси төркемнәре",
|
||||
@@ -44,6 +43,9 @@
|
||||
"Proxy detail": "Прокси турында тулы мәгълүмат",
|
||||
"Profiles": "Профильләр",
|
||||
"Update All Profiles": "Барлык профильләрне яңарту",
|
||||
"Update Channel": "Update Channel",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "Кулланылган конфигурацияне карау",
|
||||
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
|
||||
"Paste": "Кую",
|
||||
@@ -166,7 +168,6 @@
|
||||
"Table View": "Таблица күзаллау",
|
||||
"List View": "Исемлек күзаллау",
|
||||
"Close All": "Барысын да ябу",
|
||||
"Default": "Башлангыч",
|
||||
"Download Speed": "Йөкләү тизлеге",
|
||||
"Upload Speed": "Йөкләү (чыгару) тизлеге",
|
||||
"Host": "Хост",
|
||||
@@ -197,6 +198,8 @@
|
||||
"Settings": "Көйләүләр",
|
||||
"System Setting": "Система көйләүләре",
|
||||
"Tun Mode": "Tun режимы (виртуаль челтәр адаптеры)",
|
||||
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
|
||||
"Install Service": "Хезмәтне урнаштыру",
|
||||
"Reset to Default": "Башлангычка кайтару",
|
||||
"Tun Mode Info": "Tun режимы бөтен системаның трафигын тотып ала. Аны кабызган очракта системалы проксины аерым кабызу таләп ителми.",
|
||||
"Stack": "Стек",
|
||||
@@ -280,7 +283,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 +300,6 @@
|
||||
"Theme Setting": "Тема көйләүләре",
|
||||
"Primary Color": "Төп төс",
|
||||
"Secondary Color": "Икенче төс",
|
||||
"Primary Text Color": "Төп текст төсе",
|
||||
"Secondary Text Color": "Икенче текст төсе",
|
||||
"Info Color": "Мәгълүмат төсе",
|
||||
"Warning Color": "Кисәтү төсе",
|
||||
"Error Color": "Хата төсе",
|
||||
@@ -367,6 +369,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 +383,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 +452,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 көйләүләре"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
"Proxy detail": "展示节点细节",
|
||||
"Profiles": "订阅",
|
||||
"Update All Profiles": "更新所有订阅",
|
||||
"Update Channel": "更新通道",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "查看运行时订阅",
|
||||
"Reactivate Profiles": "重新激活订阅",
|
||||
"Paste": "粘贴",
|
||||
@@ -305,7 +308,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 +429,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 +441,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 +631,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 +715,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 透明代理端口"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
"Proxy detail": "展示節點細節",
|
||||
"Profiles": "訂閱",
|
||||
"Update All Profiles": "更新所有訂閱",
|
||||
"Update Channel": "更新頻道",
|
||||
"Update Channel Stable": "Stable",
|
||||
"Update Channel Autobuild": "Autobuild",
|
||||
"View Runtime Config": "查看執行時訂閱",
|
||||
"Reactivate Profiles": "重新啟用訂閱",
|
||||
"Paste": "貼上",
|
||||
@@ -628,7 +631,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 +715,7 @@
|
||||
"Allow Auto Update": "允許自動更新",
|
||||
"Menu reorder mode": "選單排序模式",
|
||||
"Unlock menu order": "解鎖選單排序",
|
||||
"Lock menu order": "鎖定選單排序"
|
||||
"Lock menu order": "鎖定選單排序",
|
||||
"Open App Log": "應用程式日誌",
|
||||
"Open Core Log": "內核日誌"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user