Merge branch 'dev' into fix/downgrade-date-grid

This commit is contained in:
oomeow
2025-11-02 23:01:13 +08:00
Unverified
104 changed files with 4465 additions and 4066 deletions

View File

@@ -90,20 +90,20 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -169,7 +169,8 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -197,6 +198,14 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -259,7 +268,8 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -272,6 +282,14 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -391,7 +409,8 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -404,6 +423,14 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -538,20 +565,20 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
#### 内置Webview2版(体积较大仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)

View File

@@ -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

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ scripts/_env.sh
.idea
.old
.eslintcache
target

View File

@@ -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

View File

@@ -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:

View File

@@ -1,6 +1,45 @@
## v2.4.3
### ✨ 新增功能
感谢 @Slinetrac, @oomeow 以及 @Lythrilla 的出色贡献
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复前端无法及时刷新操作状态
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复连接界面长时间显示后报错问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- **Mihomo(Meta) 内核升级至 v1.19.15**
- 支持前端修改日志(最大文件大小、最大保留数量)
@@ -15,8 +54,11 @@
- 允许独立控制订阅自动更新
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
- 托盘 `打开目录` 中新增 `应用日志``内核日志`
</details>
### 🚀 优化改进
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
@@ -42,37 +84,11 @@
- 优化首页当前节点对MATCH规则的支持
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
- 添加热键绑定错误的提示信息
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复连接界面长时间显示后报错问题
</details>
## v2.4.2

81
docs/CONTRIBUTING_i18n.md Normal file
View 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 repos 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.

View File

@@ -94,9 +94,10 @@ export default defineConfig([
"warn",
{
vars: "all",
varsIgnorePattern: "^_+$",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_+$",
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^ignore",
},
],

View File

@@ -26,8 +26,8 @@
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
@@ -45,17 +45,17 @@
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^7.29.9",
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.1",
"@tauri-apps/plugin-dialog": "^2.4.1",
"@tauri-apps/plugin-fs": "^2.4.3",
"@tauri-apps/plugin-http": "~2.5.3",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.2",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-http": "~2.5.4",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "2.3.3",
"@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.6",
"axios": "^1.12.2",
"dayjs": "1.11.18",
"axios": "^1.13.1",
"dayjs": "1.11.19",
"foxact": "^0.2.49",
"i18next": "^25.6.0",
"js-yaml": "^4.1.0",
@@ -67,34 +67,33 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.65.0",
"react-i18next": "16.2.1",
"react-hook-form": "^7.66.0",
"react-i18next": "16.2.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router": "^7.9.4",
"react-router": "^7.9.5",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
"types-pac": "^1.0.3"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.2.4",
"@eslint/js": "^9.38.0",
"@tauri-apps/cli": "2.9.1",
"@eslint-react/eslint-plugin": "^2.3.1",
"@eslint/js": "^9.39.0",
"@tauri-apps/cli": "2.9.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.9.1",
"@types/node": "^24.9.2",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.1.0",
"@vitejs/plugin-react-swc": "^4.2.0",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"cross-env": "^10.1.0",
"eslint": "^9.38.0",
"eslint": "^9.39.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
@@ -103,7 +102,7 @@
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
"globals": "^16.5.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"jiti": "^2.6.1",
@@ -111,19 +110,19 @@
"meta-json-schema": "^1.19.14",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"sass": "^1.93.2",
"tar": "^7.5.1",
"sass": "^1.93.3",
"tar": "^7.5.2",
"terser": "^5.44.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-monaco-editor-esm": "^2.0.2",
"vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.3"
"vitest": "^4.0.6"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"eslint --fix --max-warnings=0",
"prettier --write",
"git add"
],

977
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,6 @@
"matchCurrentVersion": "<8.0.0"
}
],
"postUpdateOptions": ["pnpmDedupe"],
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
"ignoreDeps": ["criterion"]
}

View File

@@ -18,7 +18,6 @@ import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
* 3. Use file hash to detect changes and skip unnecessary chmod/copy operations
* 4. Use --force or -f flag to force re-download and update all resources
*
* This optimization significantly reduces build time for local development
*/
const cwd = process.cwd();
@@ -56,8 +55,7 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
let target;
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
@@ -68,7 +66,9 @@ const SIDECAR_HOST = target
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0];
/* ======= Version Cache Functions ======= */
// =======================
// Version Cache
// =======================
async function loadVersionCache() {
try {
if (fs.existsSync(VERSION_CACHE_FILE)) {
@@ -80,7 +80,6 @@ async function loadVersionCache() {
}
return {};
}
async function saveVersionCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true });
@@ -90,28 +89,24 @@ async function saveVersionCache(cache) {
log_debug("Failed to save version cache:", err.message);
}
}
async function getCachedVersion(key) {
const cache = await loadVersionCache();
const cached = cache[key];
if (cached && Date.now() - cached.timestamp < 3600000) {
// 1小时内有效
log_info(`Using cached version for ${key}: ${cached.version}`);
return cached.version;
}
return null;
}
async function setCachedVersion(key, version) {
const cache = await loadVersionCache();
cache[key] = {
version,
timestamp: Date.now(),
};
cache[key] = { version, timestamp: Date.now() };
await saveVersionCache(cache);
}
/* ======= File Hash Functions ======= */
// =======================
// Hash Cache & File Hash
// =======================
async function calculateFileHash(filePath) {
try {
const fileBuffer = await fsp.readFile(filePath);
@@ -122,7 +117,6 @@ async function calculateFileHash(filePath) {
return null;
}
}
async function loadHashCache() {
try {
if (fs.existsSync(HASH_CACHE_FILE)) {
@@ -134,7 +128,6 @@ async function loadHashCache() {
}
return {};
}
async function saveHashCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true });
@@ -144,28 +137,20 @@ async function saveHashCache(cache) {
log_debug("Failed to save hash cache:", err.message);
}
}
async function hasFileChanged(filePath, targetPath) {
if (FORCE) return true;
if (!fs.existsSync(targetPath)) return true;
const hashCache = await loadHashCache();
const sourceHash = await calculateFileHash(filePath);
const targetHash = await calculateFileHash(targetPath);
if (!sourceHash || !targetHash) return true;
const cacheKey = targetPath;
const cachedHash = hashCache[cacheKey];
if (cachedHash === sourceHash && sourceHash === targetHash) {
// 文件未变化,不输出日志
return false;
}
return true;
}
async function updateHashCache(targetPath) {
const hashCache = await loadHashCache();
const hash = await calculateFileHash(targetPath);
@@ -175,18 +160,25 @@ async function updateHashCache(targetPath) {
}
}
/* ======= clash meta alpha======= */
// =======================
// Meta maps (stable & alpha)
// =======================
const META_ALPHA_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
let META_ALPHA_VERSION;
const META_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v1",
"darwin-arm64": "mihomo-darwin-arm64",
"darwin-x64": "mihomo-darwin-amd64-v1-go122",
"darwin-arm64": "mihomo-darwin-arm64-go122",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
@@ -195,9 +187,24 @@ const META_ALPHA_MAP = {
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest alpha release version from the version.txt file
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2-go122",
"darwin-arm64": "mihomo-darwin-arm64-go122",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64",
};
// =======================
// Fetch latest versions
// =======================
async function getLatestAlphaVersion() {
// 如果不强制更新,先尝试从缓存获取
if (!FORCE) {
const cached = await getCachedVersion("META_ALPHA_VERSION");
if (cached) {
@@ -205,58 +212,33 @@ async function getLatestAlphaVersion() {
return;
}
}
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
if (httpProxy) {
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
...options,
method: "GET",
});
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
if (!response.ok)
throw new Error(
`Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`,
);
META_ALPHA_VERSION = (await response.text()).trim();
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
// 保存到缓存
await setCachedVersion("META_ALPHA_VERSION", META_ALPHA_VERSION);
} catch (error) {
log_error("Error fetching latest alpha version:", error.message);
} catch (err) {
log_error("Error fetching latest alpha version:", err.message);
process.exit(1);
}
}
/* ======= clash meta stable ======= */
const META_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION;
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest release version from the version.txt file
async function getLatestReleaseVersion() {
// 如果不强制更新,先尝试从缓存获取
if (!FORCE) {
const cached = await getCachedVersion("META_VERSION");
if (cached) {
@@ -264,67 +246,57 @@ async function getLatestReleaseVersion() {
return;
}
}
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
if (httpProxy) {
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_VERSION_URL, {
...options,
method: "GET",
});
let v = await response.text();
META_VERSION = v.trim(); // Trim to remove extra whitespaces
if (!response.ok)
throw new Error(
`Failed to fetch ${META_VERSION_URL}: ${response.status}`,
);
META_VERSION = (await response.text()).trim();
log_info(`Latest release version: ${META_VERSION}`);
// 保存到缓存
await setCachedVersion("META_VERSION", META_VERSION);
} catch (error) {
log_error("Error fetching latest release version:", error.message);
} catch (err) {
log_error("Error fetching latest release version:", err.message);
process.exit(1);
}
}
/*
* check available
*/
// =======================
// Validate availability
// =======================
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
/**
* core info
*/
// =======================
// Build meta objects
// =======================
function clashMetaAlpha() {
const name = META_ALPHA_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
return {
name: "verge-mihomo-alpha",
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
exeFile: `${name}${isWin ? ".exe" : ""}`,
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
};
}
@@ -332,40 +304,83 @@ function clashMeta() {
const name = META_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
return {
name: "verge-mihomo",
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
exeFile: `${name}${isWin ? ".exe" : ""}`,
zipFile: `${name}-${META_VERSION}.${urlExt}`,
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
};
}
/**
* download sidecar and rename
*/
// =======================
// download helper (增强status + magic bytes)
// =======================
async function downloadFile(url, outPath) {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
const response = await fetch(url, {
...options,
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
if (!response.ok) {
const body = await response.text().catch(() => "");
// 将 body 写到文件以便排查(可通过临时目录查看)
await fsp.mkdir(path.dirname(outPath), { recursive: true });
await fsp.writeFile(outPath, body);
throw new Error(`Failed to download ${url}: status ${response.status}`);
}
const buf = Buffer.from(await response.arrayBuffer());
await fsp.mkdir(path.dirname(outPath), { recursive: true });
// 简单 magic 字节检查
if (url.endsWith(".gz") || url.endsWith(".tgz")) {
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
await fsp.writeFile(outPath, buf);
throw new Error(
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
);
}
} else if (url.endsWith(".zip")) {
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
await fsp.writeFile(outPath, buf);
throw new Error(
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
);
}
}
await fsp.writeFile(outPath, buf);
log_success(`download finished: ${url}`);
}
// =======================
// resolveSidecar (支持 zip / tgz / gz)
// =======================
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
const sidecarPath = path.join(sidecarDir, targetFile);
await fsp.mkdir(sidecarDir, { recursive: true });
// 检查文件是否已存在,如果存在则跳过重复下载
if (!FORCE && fs.existsSync(sidecarPath)) {
log_success(`"${name}" already exists, skipping download to save time`);
log_success(`"${name}" already exists, skipping download`);
return;
}
const tempDir = path.join(TEMP_DIR, name);
const tempZip = path.join(tempDir, zipFile);
const tempExe = path.join(tempDir, exeFile);
await fsp.mkdir(tempDir, { recursive: true });
try {
if (!fs.existsSync(tempZip)) {
await downloadFile(downloadURL, tempZip);
@@ -374,78 +389,76 @@ async function resolveSidecar(binInfo) {
if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
log_debug(`"${name}" entry name`, entry.entryName);
log_debug(`"${name}" entry: ${entry.entryName}`);
});
zip.extractAllTo(tempDir, true);
await fsp.rename(tempExe, sidecarPath);
// 尝试按 exeFile 重命名,否则找第一个可执行文件
if (fs.existsSync(tempExe)) {
await fsp.rename(tempExe, sidecarPath);
} else {
// 搜索候选
const files = await fsp.readdir(tempDir);
const candidate = files.find(
(f) =>
f === path.basename(exeFile) ||
f.endsWith(".exe") ||
!f.includes("."),
);
if (!candidate)
throw new Error(`Expected binary not found in ${tempDir}`);
await fsp.rename(path.join(tempDir, candidate), sidecarPath);
}
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
log_success(`unzip finished: "${name}"`);
} else if (zipFile.endsWith(".tgz")) {
// tgz
await fsp.mkdir(tempDir, { recursive: true });
await extract({
cwd: tempDir,
file: tempZip,
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
});
await extract({ cwd: tempDir, file: tempZip });
const files = await fsp.readdir(tempDir);
log_debug(`"${name}" files in tempDir:`, files);
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
if (extractedFile) {
const extractedFilePath = path.join(tempDir, extractedFile);
await fsp.rename(extractedFilePath, sidecarPath);
log_success(`"${name}" file renamed to "${sidecarPath}"`);
execSync(`chmod 755 ${sidecarPath}`);
log_success(`chmod binary finished: "${name}"`);
} else {
throw new Error(`Expected file not found in ${tempDir}`);
}
log_debug(`"${name}" extracted files:`, files);
// 优先寻找给定 exeFile 或已知前缀
let extracted = files.find(
(f) =>
f === path.basename(exeFile) ||
f.startsWith("虚空终端-") ||
!f.includes("."),
);
if (!extracted) extracted = files[0];
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`);
await fsp.rename(path.join(tempDir, extracted), sidecarPath);
execSync(`chmod 755 ${sidecarPath}`);
log_success(`tgz processed: "${name}"`);
} else {
// gz
// .gz
const readStream = fs.createReadStream(tempZip);
const writeStream = fs.createWriteStream(sidecarPath);
await new Promise((resolve, reject) => {
const onError = (error) => {
log_error(`"${name}" gz failed:`, error.message);
reject(error);
};
readStream
.pipe(zlib.createGunzip().on("error", onError))
.pipe(zlib.createGunzip())
.on("error", (e) => {
log_error(`gunzip error for ${name}:`, e.message);
reject(e);
})
.pipe(writeStream)
.on("finish", () => {
execSync(`chmod 755 ${sidecarPath}`);
log_success(`chmod binary finished: "${name}"`);
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
resolve();
})
.on("error", onError);
.on("error", (e) => {
log_error(`write stream error for ${name}:`, e.message);
reject(e);
});
});
log_success(`gz binary processed: "${name}"`);
}
} catch (err) {
// 需要删除文件
await fsp.rm(sidecarPath, { recursive: true, force: true });
throw err;
} finally {
// delete temp dir
await fsp.rm(tempDir, { recursive: true, force: true });
}
}
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
localPath: path.join(cwd, "scripts/set_dns.sh"),
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
localPath: path.join(cwd, "scripts/unset_dns.sh"),
});
/**
* download the file to the resources dir
*/
async function resolveResource(binInfo) {
const { file, downloadURL, localPath } = binInfo;
const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
@@ -465,12 +478,9 @@ async function resolveResource(binInfo) {
}
if (localPath) {
// 检查文件哈希是否变化
if (!(await hasFileChanged(localPath, targetPath))) {
// 文件未变化,静默跳过
return;
}
await fsp.mkdir(resDir, { recursive: true });
await fsp.copyFile(localPath, targetPath);
await updateHashCache(targetPath);
@@ -480,44 +490,17 @@ async function resolveResource(binInfo) {
log_success(`${file} finished`);
}
/**
* download file and save to `path`
*/ async function downloadFile(url, path) {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = new HttpsProxyAgent(httpProxy);
}
const response = await fetch(url, {
...options,
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
const buffer = await response.arrayBuffer();
await fsp.writeFile(path, new Uint8Array(buffer));
log_success(`download finished: ${url}`);
}
// SimpleSC.dll
// SimpleSC.dll (win plugin)
const resolvePlugin = async () => {
const url =
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
const pluginDir = path.join(process.env.APPDATA || "", "Local/NSIS");
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fsp.mkdir(pluginDir, { recursive: true });
await fsp.mkdir(tempDir, { recursive: true });
@@ -527,18 +510,33 @@ const resolvePlugin = async () => {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
log_debug(`"SimpleSC" entry name`, entry.entryName);
});
zip
.getEntries()
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName));
zip.extractAllTo(tempDir, true);
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
log_success(`unzip finished: "SimpleSC"`);
if (fs.existsSync(tempDll)) {
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
log_success(`unzip finished: "SimpleSC"`);
} else {
// 如果 dll 名称不同,尝试找到 dll
const files = await fsp.readdir(tempDir);
const dll = files.find((f) => f.toLowerCase().endsWith(".dll"));
if (dll) {
await fsp.cp(path.join(tempDir, dll), pluginPath, {
recursive: true,
force: true,
});
log_success(`unzip finished: "SimpleSC" (found ${dll})`);
} else {
throw new Error("SimpleSC.dll not found in zip");
}
}
} finally {
await fsp.rm(tempDir, { recursive: true, force: true });
}
};
// service chmod
// service chmod (保留并使用 glob)
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service*",
@@ -550,23 +548,20 @@ const resolveServicePermission = async () => {
let hasChanges = false;
for (let f of serviceExecutables) {
// 使用glob模块来处理通配符
const files = glob.sync(path.join(resDir, f));
for (let filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath);
const cacheKey = `${filePath}_chmod`;
// 检查文件哈希是否变化
if (!FORCE && hashCache[cacheKey] === currentHash) {
// 权限未变化,静默跳过
continue;
}
execSync(`chmod 755 ${filePath}`);
log_success(`chmod finished: "${filePath}"`);
// 更新哈希缓存
try {
execSync(`chmod 755 ${filePath}`);
log_success(`chmod finished: "${filePath}"`);
} catch (e) {
log_error(`chmod failed for ${filePath}:`, e.message);
}
hashCache[cacheKey] = currentHash;
hasChanges = true;
}
@@ -578,34 +573,22 @@ const resolveServicePermission = async () => {
}
};
// resolveResource 函数后添加新函数
// resolve locales (从 src/locales 复制到 resources/locales并使用 hash 检查)
async function resolveLocales() {
const srcLocalesDir = path.join(cwd, "src/locales");
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
try {
// 确保目标目录存在
await fsp.mkdir(targetLocalesDir, { recursive: true });
// 读取所有语言文件
const files = await fsp.readdir(srcLocalesDir);
// 复制每个文件,只有当哈希变化时才复制
for (const file of files) {
const srcPath = path.join(srcLocalesDir, file);
const targetPath = path.join(targetLocalesDir, file);
// 检查文件是否需要更新
if (!(await hasFileChanged(srcPath, targetPath))) {
// 文件未变化,静默跳过
continue;
}
if (!(await hasFileChanged(srcPath, targetPath))) continue;
await fsp.copyFile(srcPath, targetPath);
await updateHashCache(targetPath);
log_success(`Copied locale file: ${file}`);
}
log_success("All locale files processed successfully");
} catch (err) {
log_error("Error copying locale files:", err.message);
@@ -613,34 +596,30 @@ async function resolveLocales() {
}
}
/**
* main
*/
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
return resolveResource({
file: "clash-verge-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
return resolveResource({
file: "clash-verge-service-install" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
});
};
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
return resolveResource({
file: "clash-verge-service-uninstall" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
});
@@ -666,15 +645,27 @@ const resolveEnableLoopback = () =>
file: "enableLoopback.exe",
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const resolveWinSysproxy = () =>
resolveResource({
file: "sysproxy.exe",
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
});
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
localPath: path.join(cwd, "scripts/set_dns.sh"),
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
localPath: path.join(cwd, "scripts/unset_dns.sh"),
});
// =======================
// Tasks
// =======================
const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
name: "verge-mihomo-alpha",
func: () =>
@@ -724,11 +715,7 @@ const tasks = [
retry: 5,
macosOnly: true,
},
{
name: "locales",
func: resolveLocales,
retry: 2,
},
{ name: "locales", func: resolveLocales, retry: 2 },
];
async function runTask() {

View File

@@ -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 ? "滚动更新版发布" : "正式发布";

View File

@@ -1 +1,2 @@
avoid-breaking-exported-api = true
avoid-breaking-exported-api = true
cognitive-complexity-threshold = 25

527
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@ tauri-build = { version = "2.5.1", features = [] }
[dependencies]
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.100"
dirs = "6.0"
open = "5.3.2"
log = "0.4.28"
dunce = "1.0.5"
@@ -43,7 +42,7 @@ serde = { version = "1.0.228", features = ["derive"] }
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
regex = "1.12.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.9.1", features = [
tauri = { version = "2.9.2", features = [
"protocol-asset",
"devtools",
"tray-icon",
@@ -51,13 +50,13 @@ tauri = { version = "2.9.1", features = [
"image-png",
] }
network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.2"
tauri-plugin-dialog = "2.4.1"
tauri-plugin-fs = "2.4.3"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.1"
tauri-plugin-deep-link = "2.4.4"
tauri-plugin-window-state = "2.4.0"
tauri-plugin-shell = "2.3.3"
tauri-plugin-dialog = "2.4.2"
tauri-plugin-fs = "2.4.4"
tauri-plugin-process = "2.3.1"
tauri-plugin-clipboard-manager = "2.3.2"
tauri-plugin-deep-link = "2.4.5"
tauri-plugin-window-state = "2.4.1"
zip = "6.0.0"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
@@ -67,12 +66,8 @@ futures = "0.3.31"
sys-locale = "0.3.2"
libc = "0.2.177"
gethostname = "1.1.0"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.2"
tauri-plugin-notification = "2.3.3"
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
@@ -80,20 +75,17 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
backoff = { version = "0.4.0", features = ["tokio"] }
compact_str = { version = "0.9.0", features = ["serde"] }
tauri-plugin-http = "2.5.3"
tauri-plugin-http = "2.5.4"
flexi_logger = "0.31.7"
console-subscriber = { version = "0.4.1", optional = true }
console-subscriber = { version = "0.5.0", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
smartstring = { version = "1.0.1", features = ["serde"] }
clash_verge_service_ipc = { version = "2.0.17", features = [
clash_verge_service_ipc = { version = "2.0.21", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
# clash_verge_service_ipc = { version = "2.0.17", features = [
# "client",
# ], path = "../../clash-verge-service-ipc" }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -126,8 +118,8 @@ users = "0.11.0"
signal-hook = "0.3.18"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1"
tauri-plugin-updater = "2.9.0"
[features]
@@ -232,3 +224,4 @@ needless_raw_string_hashes = "deny" # Too many in existing code
#restriction = { level = "allow", priority = -1 }
or_fun_call = "deny"
cognitive_complexity = "deny"

View File

@@ -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);

View File

@@ -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());
}
};

View File

@@ -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()
}

View File

@@ -1,15 +1,16 @@
use std::collections::VecDeque;
use super::CmdResult;
use crate::utils::dirs;
use crate::{
cmd::StringifyErr,
config::Config,
constants,
core::{CoreManager, handle, validate::CoreConfigValidator},
};
use crate::{config::*, feat, logging, utils::logging::Type};
use compact_str::CompactString;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
/// 复制Clash环境变量
#[tauri::command]
@@ -42,10 +43,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
pub async fn change_clash_core(clash_core: String) -> CmdResult<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 {
@@ -113,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
}
};
@@ -130,7 +128,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join("dns_config.yaml");
.join(constants::files::DNS_CONFIG);
// 保存DNS配置到文件
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
@@ -153,18 +151,16 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join("dns_config.yaml");
.join(constants::files::DNS_CONFIG);
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found");
return Err("DNS config file not found".into());
}
let dns_yaml = tokio::fs::read_to_string(&dns_path)
.await
.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
})?;
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
})?;
// 解析DNS配置
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
@@ -233,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())
}
@@ -246,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());
@@ -259,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() {
@@ -275,7 +269,7 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
}
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<VecDeque<CompactString>> {
pub async fn get_clash_logs() -> CmdResult<Vec<CompactString>> {
let logs = CoreManager::global()
.get_clash_logs()
.await

View File

@@ -6,6 +6,7 @@ use crate::{logging, utils::logging::Type};
use super::UnlockItem;
use super::utils::{country_code_to_emoji, get_local_date_string};
#[allow(clippy::cognitive_complexity)]
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
let auth_header =

View File

@@ -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)
}

View File

@@ -85,7 +85,7 @@ pub async fn enhance_profiles() -> CmdResult {
match feat::enhance_profiles().await {
Ok(_) => {}
Err(e) => {
log::error!(target: "app", "{}", e);
logging!(error, Type::Cmd, "{}", e);
return Err(e.to_string().into());
}
}
@@ -99,7 +99,7 @@ pub async fn import_profile(url: std::string::String, option: Option<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 +110,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 +145,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 +161,7 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
/// 创建一个新的配置文件
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<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 +180,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 +194,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 +206,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
handle::Handle::notify_profile_changed(index);
}
Err(e) => {
log::error!(target: "app", "{}", e);
logging!(error, Type::Cmd, "{}", e);
return Err(e.to_string().into());
}
}
@@ -216,6 +214,257 @@ pub async fn delete_profile(index: String) -> CmdResult {
Ok(())
}
/// 验证新配置文件的语法
async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str()));
path.ok()
} else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
None
}
}
};
// 如果获取到文件路径检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Err(());
}
// 超时保护
let file_read_result = tokio::time::timeout(
Duration::from_secs(5),
tokio::fs::read_to_string(&file_path),
)
.await;
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
})
.await;
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, "目标配置文件语法正确");
Ok(())
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
error_msg.clone(),
);
Err(())
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
error_msg.clone(),
);
Err(())
}
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
error_msg.clone(),
);
Err(())
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
error_msg.clone(),
);
Err(())
}
}
} else {
Ok(())
}
}
/// 执行配置更新并处理结果
async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
Config::profiles()
.await
.draft_mut()
.patch_config(restore_profiles)
.stringify_err()?;
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = profiles_save_file_safe().await {
logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
}
});
logging!(info, Type::Cmd, "成功恢复到之前的配置");
Ok(())
}
async fn handle_success(current_sequence: u64, current_value: Option<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
);
Config::profiles().await.apply();
handle::Handle::refresh_clash();
if let Err(e) = Tray::global().update_tooltip().await {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu().await {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
}
if let Err(e) = profiles_save_file_safe().await {
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
}
if let Some(current) = &current_value {
logging!(
info,
Type::Cmd,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true)
}
async fn handle_validation_failure(
error_msg: String,
current_profile: Option<String>,
) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::error", error_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_update_error<E: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
logging!(
warn,
Type::Cmd,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_timeout(current_profile: Option<String>, current_sequence: u64) -> CmdResult<bool> {
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(
error,
Type::Cmd,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn perform_config_update(
current_sequence: u64,
current_value: Option<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(),
)
.await;
match update_result {
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
Ok(Err(e)) => handle_update_error(e, current_sequence).await,
Err(_) => handle_timeout(current_profile, current_sequence).await,
}
}
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
@@ -256,108 +505,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref()
&& current_profile.as_ref() != Some(new_profile)
&& validate_new_profile(new_profile).await.is_err()
{
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str()));
path.ok()
} else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
None
}
}
};
// 如果获取到文件路径检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
}
// 超时保护
let file_read_result = tokio::time::timeout(
Duration::from_secs(5),
tokio::fs::read_to_string(&file_path),
)
.await;
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
})
.await;
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
error_msg.clone(),
);
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
error_msg.clone(),
);
return Ok(false);
}
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
error_msg.clone(),
);
return Ok(false);
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
error_msg.clone(),
);
return Ok(false);
}
}
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
return Ok(false);
}
// 检查请求有效性
@@ -399,163 +550,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false);
}
// 为配置更新添加超时保护
logging!(
info,
Type::Cmd,
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
Duration::from_secs(30), // 30秒超时
CoreManager::global().update_config(),
)
.await;
// 更新配置并进行验证
match update_result {
Ok(Ok((true, _))) => {
// 内核操作完成后再次检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
logging!(
info,
Type::Cmd,
"配置更新成功,序列号: {}",
current_sequence
);
Config::profiles().await.apply();
handle::Handle::refresh_clash();
// 强制刷新代理缓存确保profile切换后立即获取最新节点数据
// crate::process::AsyncHandler::spawn(|| async move {
// if let Err(e) = super::proxy::force_refresh_proxies().await {
// log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
// }
// });
if let Err(e) = Tray::global().update_tooltip().await {
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu().await {
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
}
// 保存配置文件
if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存配置文件失败: {e}");
}
// 立即通知前端配置变更
if let Some(current) = &current_value {
logging!(
info,
Type::Cmd,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true)
}
Ok(Ok((false, error_msg))) => {
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
Config::profiles().await.discard();
// 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile {
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
// 静默恢复,不触发验证
Config::profiles()
.await
.draft_mut()
.patch_config(restore_profiles)
.stringify_err()?;
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
}
});
logging!(info, Type::Cmd, "成功恢复到之前的配置");
}
// 发送验证错误通知
handle::Handle::notice_message("config_validate::error", error_msg.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
Ok(Err(e)) => {
logging!(
warn,
Type::Cmd,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
Err(_) => {
// 超时处理
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(
error,
Type::Cmd,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
logging!(
info,
Type::Cmd,
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
prev_profile,
current_sequence
);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
Config::profiles()
.await
.draft_mut()
.patch_config(restore_profiles)
.stringify_err()?;
Config::profiles().await.apply();
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
}
perform_config_update(current_sequence, current_value, current_profile).await
}
/// 根据profile name修改profiles
@@ -575,33 +570,34 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) {
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
{
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = new_option.update_interval;
let old_allow_auto_update = old_profile
.option
.as_ref()
.and_then(|o| o.allow_auto_update);
let new_allow_auto_update = profile.option.as_ref().and_then(|o| o.allow_auto_update);
let new_allow_auto_update = new_option.allow_auto_update;
(old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update)
} else {
false
};
profiles_patch_item_safe(index.clone(), profile)
profiles_patch_item_safe(&index, &profile)
.await
.stringify_err()?;
// 如果更新间隔或允许自动更新变更,异步刷新定时器
if should_refresh_timer {
let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move {
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
if let Err(e) = crate::core::Timer::global().refresh().await {
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index_clone);
crate::core::handle::Handle::notify_timer_updated(index);
}
});
}
@@ -634,10 +630,15 @@ pub async fn view_profile(index: String) -> CmdResult {
/// 读取配置文件内容
#[tauri::command]
pub async fn read_profile_file(index: String) -> CmdResult<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)
}

View File

@@ -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())
}
}

View File

@@ -3,6 +3,7 @@ use crate::{
core::service::{self, SERVICE_MANAGER, ServiceStatus},
utils::i18n::t,
};
use smartstring::SmartString;
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
if let Err(e) = SERVICE_MANAGER
@@ -12,7 +13,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
.await
{
let emsg = format!("{} Service failed: {}", op_type, e);
return Err(t(emsg.as_str()).await.into());
return Err(SmartString::from(&*t(emsg.as_str()).await));
}
Ok(())
}

View File

@@ -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()
}

View File

@@ -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();
// 分离数据获取和异步调用

View File

@@ -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()
})
}

View File

@@ -1,9 +1,10 @@
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
cmd,
config::{PrfItem, profiles_append_item_safe},
constants::{files, timing},
core::{CoreManager, handle, validate::CoreConfigValidator},
enhance, logging,
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
enhance, logging, logging_error,
utils::{Draft, dirs, help, logging::Type},
};
use anyhow::{Result, anyhow};
@@ -53,24 +54,47 @@ impl Config {
/// 初始化订阅
pub async fn init_config() -> Result<()> {
if Self::profiles()
.await
.latest_ref()
.get_item(&"Merge".into())
.is_err()
Self::ensure_default_profile_items().await?;
// init Tun mode
if !cmd::system::is_admin().unwrap_or_default()
&& service::is_service_available().await.is_err()
{
let merge_item = PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item.clone()).await?;
let verge = Config::verge().await;
verge.draft_mut().enable_tun_mode = Some(false);
verge.apply();
let _ = tray::Tray::global().update_tray_display().await;
// 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_ref().clone();
logging_error!(Type::Core, verge_data.save_file().await);
}
if Self::profiles()
.await
.latest_ref()
.get_item(&"Script".into())
.is_err()
{
let script_item = PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item.clone()).await?;
let validation_result = Self::generate_and_validate().await?;
if let Some((msg_type, msg_content)) = validation_result {
sleep(timing::STARTUP_ERROR_DELAY).await;
handle::Handle::notice_message(msg_type, msg_content);
}
Ok(())
}
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await;
if profiles.latest_ref().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item).await?;
}
if profiles.latest_ref().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item).await?;
}
Ok(())
}
async fn generate_and_validate() -> Result<Option<(&'static str, String)>> {
// 生成运行时配置
if let Err(err) = Self::generate().await {
logging!(error, Type::Config, "生成运行时配置失败: {}", err);
@@ -81,7 +105,7 @@ impl Config {
// 生成运行时配置文件并验证
let config_result = Self::generate_file(ConfigType::Run).await;
let validation_result = if config_result.is_ok() {
if config_result.is_ok() {
// 验证配置文件
logging!(info, Type::Config, "开始验证配置");
@@ -97,12 +121,12 @@ impl Config {
CoreManager::global()
.use_default_config("config_validate::boot_error", &error_msg)
.await?;
Some(("config_validate::boot_error", error_msg))
Ok(Some(("config_validate::boot_error", error_msg)))
} else {
logging!(info, Type::Config, "配置验证成功");
// 前端没有必要知道验证成功的消息,也没有事件驱动
// Some(("config_validate::success", String::new()))
None
Ok(None)
}
}
Err(err) => {
@@ -110,7 +134,7 @@ impl Config {
CoreManager::global()
.use_default_config("config_validate::process_terminated", "")
.await?;
Some(("config_validate::process_terminated", String::new()))
Ok(Some(("config_validate::process_terminated", String::new())))
}
}
} else {
@@ -118,15 +142,8 @@ impl Config {
CoreManager::global()
.use_default_config("config_validate::error", "")
.await?;
Some(("config_validate::error", String::new()))
};
if let Some((msg_type, msg_content)) = validation_result {
sleep(timing::STARTUP_ERROR_DELAY).await;
handle::Handle::notice_message(msg_type, msg_content);
Ok(Some(("config_validate::error", String::new())))
}
Ok(())
}
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
@@ -151,11 +168,11 @@ impl Config {
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
**Config::runtime().await.draft_mut() = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
});
};
Ok(())
}

View File

@@ -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")
}
}

View 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,12 +32,24 @@ pub struct CleanupResult {
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key;
$lv.$key = $rv.$key.clone();
}
};
}
impl IProfiles {
// Helper to find and remove an item by uid from the items vec, returning its file name (if any).
fn take_item_file_by_uid(
items: &mut Vec<PrfItem>,
target_uid: Option<String>,
) -> Option<String> {
for (i, _) in items.iter().enumerate() {
if items[i].uid == target_uid {
return items.remove(i).file;
}
}
None
}
pub async fn new() -> Self {
match dirs::profiles_path() {
Ok(path) => match help::read_yaml::<Self>(&path).await {
@@ -55,12 +68,12 @@ impl IProfiles {
profiles
}
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
Self::template()
}
},
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
Self::template()
}
}
@@ -110,28 +123,30 @@ impl IProfiles {
}
/// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if let Some(items) = self.items.as_ref() {
let some_uid = Some(uid.clone());
pub fn get_item(&self, uid: impl AsRef<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
@@ -153,7 +168,7 @@ impl IProfiles {
if self.current.is_none()
&& (item.itype == Some("remote".into()) || item.itype == Some("local".into()))
{
self.current = uid;
self.current = uid.to_owned();
}
if self.items.is_none() {
@@ -161,24 +176,23 @@ impl IProfiles {
}
if let Some(items) = self.items.as_mut() {
items.push(item)
items.push(item.to_owned());
}
// self.save_file().await
Ok(())
}
/// reorder items
pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(active_id.clone()) {
if items[i].uid.as_ref() == Some(active_id) {
old_index = Some(i);
}
if items[i].uid == Some(over_id.clone()) {
if items[i].uid.as_ref() == Some(over_id) {
new_index = Some(i);
}
}
@@ -194,11 +208,11 @@ impl IProfiles {
}
/// update the item value
pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
for each in items.iter_mut() {
if each.uid == Some(uid.clone()) {
if each.uid.as_ref() == Some(uid) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
@@ -220,13 +234,13 @@ impl IProfiles {
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
// find the item
let _ = self.get_item(&uid)?;
let _ = self.get_item(uid)?;
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
@@ -235,8 +249,8 @@ impl IProfiles {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home;
each.option = PrfOption::merge(each.option.clone(), item.option);
each.home = item.home.to_owned();
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
@@ -267,115 +281,58 @@ impl IProfiles {
/// delete item
/// if delete the current then return true
pub async fn delete_item(&mut self, uid: String) -> Result<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());
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
let mut items = self.items.take().unwrap_or_default();
let mut index = None;
let mut merge_index = None;
let mut script_index = None;
let mut rules_index = None;
let mut proxies_index = None;
let mut groups_index = None;
// get the index
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
}
}
if let Some(index) = index
&& let Some(file) = items.remove(index).file
{
// remove the main item (if exists) and delete its file
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) {
let _ = dirs::app_profiles_dir()?
.join(file.as_str())
.remove_if_exists()
.await;
}
// get the merge index
for (i, _) in items.iter().enumerate() {
if items[i].uid == merge_uid {
merge_index = Some(i);
break;
}
}
if let Some(index) = merge_index
&& let Some(file) = items.remove(index).file
{
// remove related extension items (merge, script, rules, proxies, groups)
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) {
let _ = dirs::app_profiles_dir()?
.join(file.as_str())
.remove_if_exists()
.await;
}
// get the script index
for (i, _) in items.iter().enumerate() {
if items[i].uid == script_uid {
script_index = Some(i);
break;
}
}
if let Some(index) = script_index
&& let Some(file) = items.remove(index).file
{
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) {
let _ = dirs::app_profiles_dir()?
.join(file.as_str())
.remove_if_exists()
.await;
}
// get the rules index
for (i, _) in items.iter().enumerate() {
if items[i].uid == rules_uid {
rules_index = Some(i);
break;
}
}
if let Some(index) = rules_index
&& let Some(file) = items.remove(index).file
{
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) {
let _ = dirs::app_profiles_dir()?
.join(file.as_str())
.remove_if_exists()
.await;
}
// get the proxies index
for (i, _) in items.iter().enumerate() {
if items[i].uid == proxies_uid {
proxies_index = Some(i);
break;
}
}
if let Some(index) = proxies_index
&& let Some(file) = items.remove(index).file
{
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) {
let _ = dirs::app_profiles_dir()?
.join(file.as_str())
.remove_if_exists()
.await;
}
// get the groups index
for (i, _) in items.iter().enumerate() {
if items[i].uid == groups_uid {
groups_index = Some(i);
break;
}
}
if let Some(index) = groups_index
&& let Some(file) = items.remove(index).file
{
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) {
let _ = dirs::app_profiles_dir()?
.join(file.as_str())
.remove_if_exists()
.await;
}
// delete the original uid
if current == uid {
if current == *uid {
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
@@ -387,7 +344,7 @@ impl IProfiles {
self.items = Some(items);
self.save_file().await?;
Ok(current == uid)
Ok(current == *uid)
}
/// 获取current指向的订阅内容
@@ -478,8 +435,8 @@ impl IProfiles {
}
/// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: String) -> bool {
self.current == Some(index)
pub fn is_current_profile_index(&self, index: &String) -> bool {
self.current.as_ref() == Some(index)
}
/// 获取所有的profiles(uid名称)
@@ -498,6 +455,18 @@ impl IProfiles {
})
}
/// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &String) -> Option<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()?;
@@ -536,7 +505,7 @@ impl IProfiles {
{
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
continue;
}
@@ -545,11 +514,15 @@ impl IProfiles {
match path.to_path_buf().remove_if_exists().await {
Ok(_) => {
deleted_files.push(file_name.into());
log::info!(target: "app", "已清理冗余文件: {file_name}");
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}").into());
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
logging!(
warn,
Type::Config,
"Warning: 清理文件失败: {file_name} - {e}"
);
}
}
}
@@ -562,8 +535,9 @@ impl IProfiles {
failed_deletions,
};
log::info!(
target: "app",
logging!(
info,
Type::Config,
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files.len(),
@@ -671,14 +645,14 @@ impl IProfiles {
use crate::config::Config;
pub async fn profiles_append_item_with_filedata_safe(
item: PrfItem,
item: &PrfItem,
file_data: Option<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 {
@@ -688,7 +662,7 @@ pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
.await
}
pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> {
pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -698,7 +672,7 @@ pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()
.await
}
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -708,7 +682,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 {
@@ -728,7 +702,7 @@ pub async fn profiles_save_file_safe() -> Result<()> {
.await
}
pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> {
pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {

View File

@@ -1,3 +1,4 @@
use crate::config::Config;
use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging,
@@ -203,8 +204,7 @@ pub struct IVerge {
pub enable_tray_speed: Option<bool>,
pub enable_tray_icon: Option<bool>,
// pub enable_tray_icon: Option<bool>,
/// show proxy groups directly on tray root menu
pub tray_inline_proxy_groups: Option<bool>,
@@ -305,19 +305,17 @@ impl IVerge {
/// 配置修正后重新加载配置
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
use crate::config::Config;
let config_draft = Config::verge().await;
*config_draft.draft_mut() = Box::new(updated_config.clone());
config_draft.apply();
logging!(
info,
Type::Config,
"内存配置已强制更新新的clash_core: {:?}",
updated_config.clash_core
&updated_config.clash_core
);
let config_draft = Config::verge().await;
**config_draft.draft_mut() = updated_config;
config_draft.apply();
Ok(())
}
@@ -355,12 +353,12 @@ impl IVerge {
config
}
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
Self::template()
}
},
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
Self::template()
}
}
@@ -419,7 +417,7 @@ impl IVerge {
webdav_username: None,
webdav_password: None,
enable_tray_speed: Some(false),
enable_tray_icon: Some(true),
// enable_tray_icon: Some(true),
tray_inline_proxy_groups: Some(false),
enable_global_hotkey: Some(true),
enable_auto_light_weight_mode: Some(false),
@@ -438,11 +436,12 @@ impl IVerge {
/// patch verge config
/// only save to file
pub fn patch_config(&mut self, patch: IVerge) {
#[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: &IVerge) {
macro_rules! patch {
($key: tt) => {
if patch.$key.is_some() {
self.$key = patch.$key;
self.$key = patch.$key.clone();
}
};
}
@@ -514,7 +513,7 @@ impl IVerge {
patch!(webdav_username);
patch!(webdav_password);
patch!(enable_tray_speed);
patch!(enable_tray_icon);
// patch!(enable_tray_icon);
patch!(tray_inline_proxy_groups);
patch!(enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes);
@@ -608,7 +607,7 @@ pub struct IVergeResponse {
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
pub enable_tray_icon: Option<bool>,
// pub enable_tray_icon: Option<bool>,
pub tray_inline_proxy_groups: Option<bool>,
pub enable_auto_light_weight_mode: Option<bool>,
pub auto_light_weight_minutes: Option<u64>,
@@ -685,7 +684,7 @@ impl From<IVerge> for IVergeResponse {
webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password,
enable_tray_speed: verge.enable_tray_speed,
enable_tray_icon: verge.enable_tray_icon,
// enable_tray_icon: verge.enable_tray_icon,
tray_inline_proxy_groups: verge.tray_inline_proxy_groups,
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
auto_light_weight_minutes: verge.auto_light_weight_minutes,
@@ -697,3 +696,9 @@ impl From<IVerge> for IVergeResponse {
}
}
}
impl From<Box<IVerge>> for IVergeResponse {
fn from(verge: Box<IVerge>) -> Self {
IVergeResponse::from(*verge)
}
}

View File

@@ -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",

View File

@@ -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();

View File

@@ -1,4 +1,10 @@
use crate::{config::Config, utils::dirs};
use crate::constants::files::DNS_CONFIG;
use crate::{
config::Config,
logging,
process::AsyncHandler,
utils::{dirs, logging::Type},
};
use anyhow::Error;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
@@ -7,13 +13,12 @@ use smartstring::alias::String;
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
fs,
io::Write,
path::PathBuf,
sync::Arc,
time::Duration,
};
use tokio::time::timeout;
use tokio::{fs, time::timeout};
use zip::write::SimpleFileOptions;
// 应用版本常量,来自 tauri.conf.json
@@ -138,9 +143,14 @@ impl WebDavClient {
.is_err()
{
match client.mkcol(dirs::BACKUP_DIR).await {
Ok(_) => log::info!("Successfully created backup directory"),
Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"),
Err(e) => {
log::warn!("Failed to create backup directory: {}", e);
logging!(
warn,
Type::Backup,
"Warning: Failed to create backup directory: {}",
e
);
// 清除缓存,强制下次重新尝试
self.reset();
return Err(anyhow::Error::msg(format!(
@@ -170,7 +180,7 @@ impl WebDavClient {
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
// 读取文件并上传,如果失败尝试一次重试
let file_content = fs::read(&file_path)?;
let file_content = fs::read(&file_path).await?;
// 添加超时保护
let upload_result = timeout(
@@ -181,7 +191,11 @@ impl WebDavClient {
match upload_result {
Err(_) => {
log::warn!("Upload timed out, retrying once");
logging!(
warn,
Type::Backup,
"Warning: Upload timed out, retrying once"
);
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -192,7 +206,11 @@ impl WebDavClient {
}
Ok(Err(e)) => {
log::warn!("Upload failed, retrying once: {e}");
logging!(
warn,
Type::Backup,
"Warning: Upload failed, retrying once: {e}"
);
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -212,7 +230,7 @@ impl WebDavClient {
let fut = async {
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
fs::write(&storage_path, &content).await?;
Ok::<(), Error>(())
};
@@ -250,18 +268,19 @@ impl WebDavClient {
}
}
pub fn create_backup() -> Result<(String, PathBuf), Error> {
pub async fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name: String = format!("{OS}-backup-{now}.zip").into();
let zip_path = temp_dir().join(zip_file_name.as_str());
let file = fs::File::create(&zip_path)?;
let value = zip_path.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::create(&value)).await??;
let mut zip = zip::ZipWriter::new(file);
zip.add_directory("profiles/", SimpleFileOptions::default())?;
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
for entry in entries {
let entry = entry?;
if let Ok(mut entries) = fs::read_dir(dirs::app_profiles_dir()?).await {
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_file() {
let file_name_os = entry.file_name();
@@ -270,16 +289,16 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
let backup_path = format!("profiles/{}", file_name);
zip.start_file(backup_path, options)?;
let file_content = fs::read(&path)?;
let file_content = fs::read(&path).await?;
zip.write_all(&file_content)?;
}
}
}
zip.start_file(dirs::CLASH_CONFIG, options)?;
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
zip.write_all(fs::read(dirs::clash_path()?).await?.as_slice())?;
let mut verge_config: serde_json::Value =
serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
let verge_text = fs::read_to_string(dirs::verge_path()?).await?;
let mut verge_config: serde_json::Value = serde_yaml_ng::from_str(&verge_text)?;
if let Some(obj) = verge_config.as_object_mut() {
obj.remove("webdav_username");
obj.remove("webdav_password");
@@ -288,14 +307,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
let dns_config_path = dirs::app_home_dir()?.join(dirs::DNS_CONFIG);
let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG);
if dns_config_path.exists() {
zip.start_file(dirs::DNS_CONFIG, options)?;
zip.write_all(fs::read(&dns_config_path)?.as_slice())?;
zip.start_file(DNS_CONFIG, options)?;
zip.write_all(fs::read(&dns_config_path).await?.as_slice())?;
}
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
zip.write_all(fs::read(dirs::profiles_path()?).await?.as_slice())?;
zip.finish()?;
Ok((zip_file_name, zip_path))
}

View File

@@ -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");

View File

@@ -8,7 +8,6 @@ use anyhow::{Result, bail};
use parking_lot::Mutex;
use smartstring::alias::String;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
/// Enum representing all available hotkey functions
@@ -105,66 +104,53 @@ impl Hotkey {
}
/// Execute the function associated with a hotkey function enum
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
let app_handle = app_handle.clone();
fn execute_function(function: HotkeyFunction) {
match function {
HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || {
crate::feat::open_or_close_dashboard().await;
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
notify_event(NotificationEvent::DashboardToggled).await;
});
}
HotkeyFunction::ClashModeRule => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("rule".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
)
.await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await;
});
}
HotkeyFunction::ClashModeGlobal => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("global".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
)
.await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await;
});
}
HotkeyFunction::ClashModeDirect => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("direct".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
)
.await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await;
});
}
HotkeyFunction::ToggleSystemProxy => {
AsyncHandler::spawn(async move || {
feat::toggle_system_proxy().await;
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
notify_event(NotificationEvent::SystemProxyToggled).await;
});
}
HotkeyFunction::ToggleTunMode => {
AsyncHandler::spawn(async move || {
feat::toggle_tun_mode(None).await;
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
notify_event(NotificationEvent::TunModeToggled).await;
});
}
HotkeyFunction::EntryLightweightMode => {
AsyncHandler::spawn(async move || {
entry_lightweight_mode().await;
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
notify_event(NotificationEvent::LightweightModeEntered).await;
});
}
HotkeyFunction::Quit => {
AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::AppQuit).await;
notify_event(NotificationEvent::AppQuit).await;
feat::quit().await;
});
}
@@ -172,7 +158,7 @@ impl Hotkey {
HotkeyFunction::Hide => {
AsyncHandler::spawn(async move || {
feat::hide().await;
notify_event(app_handle, NotificationEvent::AppHidden).await;
notify_event(NotificationEvent::AppHidden).await;
});
}
}
@@ -224,14 +210,12 @@ impl Hotkey {
let is_quit = matches!(function, HotkeyFunction::Quit);
manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
let hotkey_event_owned = *hotkey_event;
let event_owned = event;
let function_owned = function;
let is_quit_owned = is_quit;
let app_handle_cloned = app_handle.clone();
AsyncHandler::spawn(move || async move {
if event_owned.state == ShortcutState::Pressed {
logging!(
@@ -242,11 +226,11 @@ impl Hotkey {
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = app_handle_cloned.get_webview_window("main")
if let Some(window) = handle::Handle::get_window()
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
Self::execute_function(function_owned);
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
@@ -258,14 +242,14 @@ impl Hotkey {
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function_owned, &app_handle_cloned);
Self::execute_function(function_owned);
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
Self::execute_function(function_owned, &app_handle_cloned);
Self::execute_function(function_owned);
}
}
}

View File

@@ -1,38 +1,6 @@
use std::{collections::VecDeque, sync::Arc};
use std::sync::Arc;
use compact_str::CompactString;
use once_cell::sync::OnceCell;
use parking_lot::{RwLock, RwLockReadGuard};
use clash_verge_logger::AsyncLogger;
use once_cell::sync::Lazy;
const LOGS_QUEUE_LEN: usize = 100;
pub struct ClashLogger {
logs: Arc<RwLock<VecDeque<CompactString>>>,
}
impl ClashLogger {
pub fn global() -> &'static ClashLogger {
static LOGGER: OnceCell<ClashLogger> = OnceCell::new();
LOGGER.get_or_init(|| ClashLogger {
logs: Arc::new(RwLock::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
})
}
pub fn get_logs(&self) -> RwLockReadGuard<'_, VecDeque<CompactString>> {
self.logs.read()
}
pub fn append_log(&self, text: CompactString) {
let mut logs = self.logs.write();
if logs.len() > LOGS_QUEUE_LEN {
logs.pop_front();
}
logs.push_back(text);
}
pub fn clear_logs(&self) {
let mut logs = self.logs.write();
logs.clear();
}
}
pub static CLASH_LOGGER: Lazy<Arc<AsyncLogger>> = Lazy::new(|| Arc::new(AsyncLogger::new()));

View File

@@ -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);

View File

@@ -1,7 +1,8 @@
use super::{CoreManager, RunningMode};
use crate::config::{Config, ConfigType, IVerge};
use crate::{
core::{
logger::ClashLogger,
logger::CLASH_LOGGER,
service::{SERVICE_MANAGER, ServiceStatus},
},
logging,
@@ -14,16 +15,16 @@ impl CoreManager {
pub async fn start_core(&self) -> Result<()> {
self.prepare_startup().await?;
match self.get_running_mode() {
match *self.get_running_mode() {
RunningMode::Service => self.start_core_by_service().await,
RunningMode::NotRunning | RunningMode::Sidecar => self.start_core_by_sidecar().await,
}
}
pub async fn stop_core(&self) -> Result<()> {
ClashLogger::global().clear_logs();
CLASH_LOGGER.clear_logs().await;
match self.get_running_mode() {
match *self.get_running_mode() {
RunningMode::Service => self.stop_core_by_service().await,
RunningMode::Sidecar => self.stop_core_by_sidecar(),
RunningMode::NotRunning => Ok(()),
@@ -41,18 +42,12 @@ impl CoreManager {
self.start_core().await
}
pub async fn change_core(&self, clash_core: Option<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();

View File

@@ -1,6 +1,5 @@
mod config;
mod lifecycle;
mod process;
mod state;
use anyhow::Result;
@@ -11,7 +10,7 @@ use tokio::sync::Semaphore;
use crate::process::CommandChildGuard;
use crate::singleton_lazy;
#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq)]
#[derive(Debug, serde::Serialize, PartialEq, Eq)]
pub enum RunningMode {
Service,
Sidecar,
@@ -37,14 +36,14 @@ pub struct CoreManager {
#[derive(Debug)]
struct State {
running_mode: RunningMode,
running_mode: Arc<RunningMode>,
child_sidecar: Option<CommandChildGuard>,
}
impl Default for State {
fn default() -> Self {
Self {
running_mode: RunningMode::NotRunning,
running_mode: Arc::new(RunningMode::NotRunning),
child_sidecar: None,
}
}
@@ -61,16 +60,19 @@ impl Default for CoreManager {
}
impl CoreManager {
pub fn get_running_mode(&self) -> RunningMode {
self.state.lock().running_mode
pub fn get_running_mode(&self) -> Arc<RunningMode> {
Arc::clone(&self.state.lock().running_mode)
}
pub fn set_running_mode(&self, mode: RunningMode) {
self.state.lock().running_mode = mode;
self.state.lock().running_mode = Arc::new(mode);
}
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
self.state.lock().child_sidecar = Some(child);
}
pub async fn init(&self) -> Result<()> {
self.cleanup_orphaned_processes().await?;
self.start_core().await?;
Ok(())
}

View File

@@ -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())
}
}
}

View File

@@ -2,7 +2,7 @@ use super::{CoreManager, RunningMode};
use crate::{
AsyncHandler,
config::Config,
core::{handle, logger::ClashLogger, service},
core::{handle, logger::CLASH_LOGGER, service},
logging,
process::CommandChildGuard,
utils::{
@@ -15,15 +15,15 @@ use anyhow::Result;
use compact_str::CompactString;
use flexi_logger::DeferredNow;
use log::Level;
use std::collections::VecDeque;
use scopeguard::defer;
use tauri_plugin_shell::ShellExt;
impl CoreManager {
pub async fn get_clash_logs(&self) -> Result<VecDeque<CompactString>> {
match self.get_running_mode() {
pub async fn get_clash_logs(&self) -> Result<Vec<CompactString>> {
match *self.get_running_mode() {
RunningMode::Service => service::get_clash_logs_by_service().await,
RunningMode::Sidecar => Ok(ClashLogger::global().get_logs().clone()),
RunningMode::NotRunning => Ok(VecDeque::new()),
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().await),
RunningMode::NotRunning => Ok(Vec::new()),
}
}
@@ -49,11 +49,8 @@ impl CoreManager {
let pid = child.pid();
logging!(trace, Type::Core, "Sidecar started with PID: {}", pid);
{
let mut state = self.state.lock();
state.child_sidecar = Some(CommandChildGuard::new(child));
state.running_mode = RunningMode::Sidecar;
}
self.set_running_child_sidecar(CommandChildGuard::new(child));
self.set_running_mode(RunningMode::Sidecar);
let shared_writer: SharedWriter =
std::sync::Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
@@ -67,7 +64,7 @@ impl CoreManager {
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
let w = shared_writer.lock().await;
write_sidecar_log(w, &mut now, Level::Error, &message);
ClashLogger::global().append_log(message);
CLASH_LOGGER.append_log(message).await;
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let mut now = DeferredNow::default();
@@ -80,7 +77,7 @@ impl CoreManager {
};
let w = shared_writer.lock().await;
write_sidecar_log(w, &mut now, Level::Info, &message);
ClashLogger::global().clear_logs();
CLASH_LOGGER.clear_logs().await;
break;
}
_ => {}
@@ -93,14 +90,15 @@ impl CoreManager {
pub(super) fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(info, Type::Core, "Stopping sidecar");
defer! {
self.set_running_mode(RunningMode::NotRunning);
}
let mut state = self.state.lock();
if let Some(child) = state.child_sidecar.take() {
let pid = child.pid();
drop(child);
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);
}
state.running_mode = RunningMode::NotRunning;
Ok(())
}
@@ -114,8 +112,10 @@ impl CoreManager {
pub(super) async fn stop_core_by_service(&self) -> Result<()> {
logging!(info, Type::Core, "Stopping service");
defer! {
self.set_running_mode(RunningMode::NotRunning);
}
service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
}
}

View File

@@ -96,10 +96,17 @@ impl NotificationSystem {
let handle = Handle::global();
while !handle.is_exiting() {
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
match rx.recv() {
Ok(event) => Self::process_event(handle, event),
Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => break,
Err(e) => {
logging!(
error,
Type::System,
"receive event error, stop notification worker: {}",
e
);
break;
}
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::{
config::Config,
core::tray,
logging, logging_error,
utils::{dirs, init::service_writer_config, logging::Type},
};
@@ -8,7 +9,6 @@ use clash_verge_service_ipc::CoreConfig;
use compact_str::CompactString;
use once_cell::sync::Lazy;
use std::{
collections::VecDeque,
env::current_exe,
path::{Path, PathBuf},
process::Command as StdCommand,
@@ -394,7 +394,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
start_with_existing_service(config_file).await
}
pub(super) async fn get_clash_logs_by_service() -> Result<VecDeque<CompactString>> {
pub(super) async fn get_clash_logs_by_service() -> Result<Vec<CompactString>> {
logging!(info, Type::Service, "正在获取服务模式下的 Clash 日志");
let response = clash_verge_service_ipc::get_clash_logs()
@@ -532,6 +532,7 @@ impl ServiceManager {
return Err(anyhow::anyhow!("服务不可用: {}", reason));
}
}
let _ = tray::Tray::global().update_tray_display().await;
Ok(())
}
}

View File

@@ -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))
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,49 @@
use crate::utils::i18n::t;
use std::sync::Arc;
macro_rules! define_menu {
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
#[derive(Debug)]
pub struct MenuTexts {
$(pub $field: Arc<str>,)+
}
pub struct MenuIds;
impl MenuTexts {
pub async fn new() -> Self {
let ($($field,)+) = futures::join!($(t($text),)+);
Self { $($field,)+ }
}
}
impl MenuIds {
$(pub const $const_name: &'static str = $id;)+
}
};
}
define_menu! {
dashboard => DASHBOARD, "tray_dashboard", "Dashboard",
rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode",
global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode",
direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode",
profiles => PROFILES, "tray_profiles", "Profiles",
proxies => PROXIES, "tray_proxies", "Proxies",
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy",
tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode",
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections",
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode",
copy_env => COPY_ENV, "tray_copy_env", "Copy Env",
conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir",
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
app_log => APP_LOG, "tray_app_log", "Open App Log",
core_log => CORE_LOG, "tray_core_log", "Open Core Log",
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",
more => MORE, "tray_more", "More",
exit => EXIT, "tray_exit", "Exit",
}

View File

@@ -1,8 +1,12 @@
use once_cell::sync::OnceCell;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
use tauri_plugin_mihomo::models::Proxies;
use tokio::fs;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::config::PrfSelected;
use crate::core::service;
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
@@ -21,8 +25,8 @@ use futures::future::join_all;
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
use std::sync::Arc;
use std::{
fs,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
@@ -31,9 +35,13 @@ use tauri::{
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconEvent},
};
mod menu_def;
use menu_def::{MenuIds, MenuTexts};
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
type ProxyMenuItem = (Option<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
#[derive(Clone)]
struct TrayState {}
@@ -54,8 +62,12 @@ fn should_handle_tray_click() -> bool {
*last_click = now;
true
} else {
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
now.duration_since(*last_click).as_millis());
logging!(
debug,
Type::Tray,
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
now.duration_since(*last_click).as_millis()
);
false
}
}
@@ -78,7 +90,7 @@ impl TrayState {
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
&& let Ok(icon_data) = fs::read(common_icon_path)
&& let Ok(icon_data) = fs::read(common_icon_path).await
{
return (true, icon_data);
}
@@ -115,7 +127,7 @@ impl TrayState {
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
&& let Ok(icon_data) = fs::read(sysproxy_icon_path)
&& let Ok(icon_data) = fs::read(sysproxy_icon_path).await
{
return (true, icon_data);
}
@@ -152,7 +164,7 @@ impl TrayState {
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
&& let Ok(icon_data) = fs::read(tun_icon_path)
&& let Ok(icon_data) = fs::read(tun_icon_path).await
{
return (true, icon_data);
}
@@ -199,7 +211,7 @@ singleton_lazy!(Tray, TRAY, Tray::default);
impl Tray {
pub async fn init(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘初始化");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化");
return Ok(());
}
@@ -207,11 +219,15 @@ impl Tray {
match self.create_tray_from_handle(app_handle).await {
Ok(_) => {
log::info!(target: "app", "System tray created successfully");
logging!(info, Type::Tray, "System tray created successfully");
}
Err(e) => {
// Don't return error, let application continue running without tray
log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e);
logging!(
warn,
Type::Tray,
"System tray creation failed: {e}, Application will continue running without tray icon",
);
}
}
// TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白
@@ -222,7 +238,7 @@ impl Tray {
/// 更新托盘点击行为
pub async fn update_click_behavior(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘点击行为更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘点击行为更新");
return Ok(());
}
@@ -242,7 +258,7 @@ impl Tray {
/// 更新托盘菜单
pub async fn update_menu(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘菜单更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新");
return Ok(());
}
// 调整最小更新间隔,确保状态及时刷新
@@ -290,6 +306,8 @@ impl Tray {
let verge = Config::verge().await.latest_ref().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
|| service::is_service_available().await.is_ok();
let mode = {
Config::clash()
.await
@@ -315,16 +333,21 @@ impl Tray {
Some(mode.as_str()),
*system_proxy,
*tun_mode,
tun_mode_available,
profile_uid_and_name,
is_lightweight_mode,
)
.await?,
));
log::debug!(target: "app", "托盘菜单更新成功");
logging!(debug, Type::Tray, "托盘菜单更新成功");
Ok(())
}
None => {
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray menu: tray not found"
);
Ok(())
}
}
@@ -334,7 +357,7 @@ impl Tray {
#[cfg(target_os = "macos")]
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
return Ok(());
}
@@ -343,7 +366,11 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray icon: tray not found"
);
return Ok(());
}
};
@@ -373,7 +400,7 @@ impl Tray {
#[cfg(not(target_os = "macos"))]
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
return Ok(());
}
@@ -382,7 +409,11 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray icon: tray not found"
);
return Ok(());
}
};
@@ -405,7 +436,7 @@ impl Tray {
/// 更新托盘显示状态的函数
pub async fn update_tray_display(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘显示状态更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新");
return Ok(());
}
@@ -423,7 +454,7 @@ impl Tray {
/// 更新托盘提示
pub async fn update_tooltip(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘提示更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘提示更新");
return Ok(());
}
@@ -479,7 +510,11 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&tooltip));
} else {
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
logging!(
warn,
Type::Tray,
"Failed to update tray tooltip: tray not found"
);
}
Ok(())
@@ -487,7 +522,7 @@ impl Tray {
pub async fn update_part(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘局部更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
return Ok(());
}
// self.update_menu().await?;
@@ -500,11 +535,11 @@ impl Tray {
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘创建");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建");
return Ok(());
}
log::info!(target: "app", "正在从AppHandle创建系统托盘");
logging!(info, Type::Tray, "正在从AppHandle创建系统托盘");
// 获取图标
let icon_bytes = TrayState::get_common_tray_icon().await.1;
@@ -550,7 +585,7 @@ impl Tray {
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
log::debug!(target: "app", "tray event: {tray_event:?}");
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
if let TrayIconEvent::Click {
button: MouseButton::Left,
@@ -585,14 +620,13 @@ impl Tray {
});
});
tray.on_menu_event(on_menu_event);
log::info!(target: "app", "系统托盘创建成功");
Ok(())
}
// 托盘统一的状态更新函数
pub async fn update_all_states(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘状态更新");
logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新");
return Ok(());
}
@@ -606,70 +640,8 @@ impl Tray {
}
}
async fn create_tray_menu(
app_handle: &AppHandle,
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
profile_uid_and_name: Vec<(String, String)>,
is_lightweight_mode: bool,
) -> Result<tauri::menu::Menu<Wry>> {
let mode = mode.unwrap_or("");
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_ref();
profiles_ref
.get_current()
.and_then(|uid| profiles_ref.get_item(&uid).ok())
.and_then(|profile| profile.selected.clone())
.unwrap_or_default()
};
let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await;
let runtime_proxy_groups_order = cmd::get_runtime_config()
.await
.map_err(|e| {
logging!(
error,
Type::Cmd,
"Failed to fetch runtime proxy groups for tray menu: {e}"
);
})
.ok()
.flatten()
.map(|config| {
config
.get("proxy-groups")
.and_then(|groups| groups.as_sequence())
.map(|groups| {
groups
.iter()
.filter_map(|group| group.get("name"))
.filter_map(|name| name.as_str())
.map(|name| name.into())
.collect::<Vec<String>>()
})
.unwrap_or_default()
});
let proxy_group_order_map = runtime_proxy_groups_order.as_ref().map(|group_names| {
group_names
.iter()
.enumerate()
.map(|(index, name)| (name.clone(), index))
.collect::<HashMap<String, usize>>()
});
let verge_settings = Config::verge().await.latest_ref().clone();
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
let version = env!("CARGO_PKG_VERSION");
let hotkeys = verge_settings
.hotkeys
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
hotkeys
.as_ref()
.map(|h| {
h.iter()
@@ -689,35 +661,43 @@ async fn create_tray_menu(
})
.collect::<std::collections::HashMap<String, String>>()
})
.unwrap_or_default();
.unwrap_or_default()
}
let profile_menu_items: Vec<CheckMenuItem<Wry>> = {
let futures = profile_uid_and_name
.iter()
.map(|(profile_uid, profile_name)| {
let app_handle = app_handle.clone();
let profile_uid = profile_uid.clone();
let profile_name = profile_name.clone();
async move {
let is_current_profile = Config::profiles()
.await
.data_mut()
.is_current_profile_index(profile_uid.clone());
CheckMenuItem::with_id(
&app_handle,
format!("profiles_{profile_uid}"),
t(&profile_name).await,
true,
is_current_profile,
None::<&str>,
)
}
});
let results = join_all(futures).await;
results.into_iter().collect::<Result<Vec<_>, _>>()?
};
async fn create_profile_menu_item(
app_handle: &AppHandle,
profile_uid_and_name: Vec<(String, String)>,
) -> Result<Vec<CheckMenuItem<Wry>>> {
let futures = profile_uid_and_name
.iter()
.map(|(profile_uid, profile_name)| {
let app_handle = app_handle.clone();
async move {
let is_current_profile = Config::profiles()
.await
.latest_ref()
.is_current_profile_index(profile_uid);
CheckMenuItem::with_id(
&app_handle,
format!("profiles_{profile_uid}"),
t(profile_name).await,
true,
is_current_profile,
None::<&str>,
)
}
});
let results = join_all(futures).await;
Ok(results.into_iter().collect::<Result<Vec<_>, _>>()?)
}
// 代理组子菜单
fn create_subcreate_proxy_menu_item(
app_handle: &AppHandle,
proxy_mode: &str,
current_profile_selected: &[PrfSelected],
proxy_group_order_map: Option<HashMap<String, usize>>,
proxy_nodes_data: Result<Proxies>,
) -> Result<Vec<Submenu<Wry>>> {
let proxy_submenus: Vec<Submenu<Wry>> = {
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
@@ -725,7 +705,7 @@ async fn create_tray_menu(
if let Ok(proxy_nodes_data) = proxy_nodes_data {
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
// Filter groups based on mode
let should_show = match mode {
let should_show = match proxy_mode {
"global" => group_name == "GLOBAL",
_ => group_name != "GLOBAL",
} &&
@@ -771,7 +751,9 @@ async fn create_tray_menu(
is_selected,
None::<&str>,
)
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
.map_err(|e| {
logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)
})
.ok()
})
.collect();
@@ -781,7 +763,7 @@ async fn create_tray_menu(
}
// Determine if group is active
let is_group_active = match mode {
let is_group_active = match proxy_mode {
"global" => group_name == "GLOBAL" && !now_proxy.is_empty(),
"direct" => false,
_ => {
@@ -813,7 +795,12 @@ async fn create_tray_menu(
let insertion_index = submenus.len();
submenus.push((group_name.into(), insertion_index, submenu));
} else {
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
logging!(
warn,
Type::Tray,
"Failed to create proxy group submenu: {}",
group_name
);
}
}
}
@@ -837,29 +824,118 @@ async fn create_tray_menu(
.map(|(_, _, submenu)| submenu)
.collect()
};
Ok(proxy_submenus)
}
fn create_proxy_menu_item(
app_handle: &AppHandle,
show_proxy_groups_inline: bool,
proxy_submenus: Vec<Submenu<Wry>>,
proxies_text: &Arc<str>,
) -> Result<ProxyMenuItem> {
// 创建代理主菜单
let (proxies_submenu, inline_proxy_items) = if show_proxy_groups_inline {
(
None,
proxy_submenus
.into_iter()
.map(|submenu| Box::new(submenu) as Box<dyn IsMenuItem<Wry>>)
.collect(),
)
} else if !proxy_submenus.is_empty() {
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
.iter()
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
.collect();
(
Some(Submenu::with_id_and_items(
app_handle,
MenuIds::PROXIES,
proxies_text,
true,
&proxy_submenu_refs,
)?),
Vec::new(),
)
} else {
(None, Vec::new())
};
Ok((proxies_submenu, inline_proxy_items))
}
async fn create_tray_menu(
app_handle: &AppHandle,
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
tun_mode_available: bool,
profile_uid_and_name: Vec<(String, String)>,
is_lightweight_mode: bool,
) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = mode.unwrap_or("");
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_ref();
profiles_ref
.get_current()
.and_then(|uid| profiles_ref.get_item(&uid).ok())
.and_then(|profile| profile.selected.clone())
.unwrap_or_default()
};
let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await;
let runtime_proxy_groups_order = cmd::get_runtime_config()
.await
.map_err(|e| {
logging!(
error,
Type::Cmd,
"Failed to fetch runtime proxy groups for tray menu: {e}"
);
})
.ok()
.flatten()
.map(|config| {
config
.get("proxy-groups")
.and_then(|groups| groups.as_sequence())
.map(|groups| {
groups
.iter()
.filter_map(|group| group.get("name"))
.filter_map(|name| name.as_str())
.map(|name| name.into())
.collect::<Vec<String>>()
})
.unwrap_or_default()
});
let proxy_group_order_map: Option<
HashMap<smartstring::SmartString<smartstring::LazyCompact>, usize>,
> = runtime_proxy_groups_order.as_ref().map(|group_names| {
group_names
.iter()
.enumerate()
.map(|(index, name)| (name.clone(), index))
.collect::<HashMap<String, usize>>()
});
let verge_settings = Config::verge().await.latest_ref().clone();
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
let version = env!("CARGO_PKG_VERSION");
let hotkeys = create_hotkeys(&verge_settings.hotkeys);
let profile_menu_items: Vec<CheckMenuItem<Wry>> =
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
// Pre-fetch all localized strings
let dashboard_text = t("Dashboard").await;
let rule_mode_text = t("Rule Mode").await;
let global_mode_text = t("Global Mode").await;
let direct_mode_text = t("Direct Mode").await;
let profiles_text = t("Profiles").await;
let proxies_text = t("Proxies").await;
let system_proxy_text = t("System Proxy").await;
let tun_mode_text = t("TUN Mode").await;
let close_all_connections_text = t("Close All Connections").await;
let lightweight_mode_text = t("LightWeight Mode").await;
let copy_env_text = t("Copy Env").await;
let conf_dir_text = t("Conf Dir").await;
let core_dir_text = t("Core Dir").await;
let logs_dir_text = t("Logs Dir").await;
let open_dir_text = t("Open Dir").await;
let restart_clash_text = t("Restart Clash Core").await;
let restart_app_text = t("Restart App").await;
let verge_version_text = t("Verge Version").await;
let more_text = t("More").await;
let exit_text = t("Exit").await;
let texts = &MenuTexts::new().await;
// Convert to references only when needed
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
.iter()
@@ -868,81 +944,66 @@ async fn create_tray_menu(
let open_window = &MenuItem::with_id(
app_handle,
"open_window",
dashboard_text,
MenuIds::DASHBOARD,
&texts.dashboard,
true,
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
)?;
let rule_mode = &CheckMenuItem::with_id(
app_handle,
"rule_mode",
rule_mode_text,
MenuIds::RULE_MODE,
&texts.rule_mode,
true,
mode == "rule",
current_proxy_mode == "rule",
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
)?;
let global_mode = &CheckMenuItem::with_id(
app_handle,
"global_mode",
global_mode_text,
MenuIds::GLOBAL_MODE,
&texts.global_mode,
true,
mode == "global",
current_proxy_mode == "global",
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
)?;
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
direct_mode_text,
MenuIds::DIRECT_MODE,
&texts.direct_mode,
true,
mode == "direct",
current_proxy_mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)?;
let profiles = &Submenu::with_id_and_items(
app_handle,
"profiles",
profiles_text,
MenuIds::PROFILES,
&texts.profiles,
true,
&profile_menu_items_refs,
)?;
// 创建代理主菜单
let (proxies_submenu, inline_proxy_items): (Option<Submenu<Wry>>, Vec<&dyn IsMenuItem<Wry>>) =
if show_proxy_groups_inline {
(
None,
proxy_submenus
.iter()
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
.collect(),
)
} else if !proxy_submenus.is_empty() {
let proxy_submenu_refs: Vec<&dyn IsMenuItem<Wry>> = proxy_submenus
.iter()
.map(|submenu| submenu as &dyn IsMenuItem<Wry>)
.collect();
let proxy_sub_menus = create_subcreate_proxy_menu_item(
app_handle,
current_proxy_mode,
&current_profile_selected,
proxy_group_order_map,
proxy_nodes_data.map_err(anyhow::Error::from),
)?;
(
Some(Submenu::with_id_and_items(
app_handle,
"proxies",
proxies_text,
true,
&proxy_submenu_refs,
)?),
Vec::new(),
)
} else {
(None, Vec::new())
};
let (proxies_menu, inline_proxy_items) = create_proxy_menu_item(
app_handle,
show_proxy_groups_inline,
proxy_sub_menus,
&texts.proxies,
)?;
let system_proxy = &CheckMenuItem::with_id(
app_handle,
"system_proxy",
system_proxy_text,
MenuIds::SYSTEM_PROXY,
&texts.system_proxy,
true,
system_proxy_enabled,
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
@@ -950,92 +1011,120 @@ async fn create_tray_menu(
let tun_mode = &CheckMenuItem::with_id(
app_handle,
"tun_mode",
tun_mode_text,
true,
MenuIds::TUN_MODE,
&texts.tun_mode,
tun_mode_available,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
)?;
let close_all_connections = &MenuItem::with_id(
app_handle,
"close_all_connections",
close_all_connections_text,
MenuIds::CLOSE_ALL_CONNECTIONS,
&texts.close_all_connections,
true,
None::<&str>,
)?;
let lighteweight_mode = &CheckMenuItem::with_id(
let lightweight_mode = &CheckMenuItem::with_id(
app_handle,
"entry_lightweight_mode",
lightweight_mode_text,
MenuIds::LIGHTWEIGHT_MODE,
&texts.lightweight_mode,
true,
is_lightweight_mode,
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
)?;
let copy_env = &MenuItem::with_id(app_handle, "copy_env", copy_env_text, true, None::<&str>)?;
let copy_env = &MenuItem::with_id(
app_handle,
MenuIds::COPY_ENV,
&texts.copy_env,
true,
None::<&str>,
)?;
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
conf_dir_text,
MenuIds::CONF_DIR,
&texts.conf_dir,
true,
None::<&str>,
)?;
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
core_dir_text,
MenuIds::CORE_DIR,
&texts.core_dir,
true,
None::<&str>,
)?;
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
logs_dir_text,
MenuIds::LOGS_DIR,
&texts.logs_dir,
true,
None::<&str>,
)?;
let open_app_log = &MenuItem::with_id(
app_handle,
MenuIds::APP_LOG,
&texts.app_log,
true,
None::<&str>,
)?;
let open_core_log = &MenuItem::with_id(
app_handle,
MenuIds::CORE_LOG,
&texts.core_log,
true,
None::<&str>,
)?;
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
open_dir_text,
MenuIds::OPEN_DIR,
&texts.open_dir,
true,
&[open_app_dir, open_core_dir, open_logs_dir],
&[
open_app_dir,
open_core_dir,
open_logs_dir,
open_app_log,
open_core_log,
],
)?;
let restart_clash = &MenuItem::with_id(
app_handle,
"restart_clash",
restart_clash_text,
MenuIds::RESTART_CLASH,
&texts.restart_clash,
true,
None::<&str>,
)?;
let restart_app = &MenuItem::with_id(
app_handle,
"restart_app",
restart_app_text,
MenuIds::RESTART_APP,
&texts.restart_app,
true,
None::<&str>,
)?;
let app_version = &MenuItem::with_id(
app_handle,
"app_version",
format!("{} {version}", verge_version_text),
MenuIds::VERGE_VERSION,
format!("{} {version}", &texts.verge_version),
true,
None::<&str>,
)?;
let more = &Submenu::with_id_and_items(
app_handle,
"more",
more_text,
MenuIds::MORE,
&texts.more,
true,
&[
close_all_connections,
@@ -1045,7 +1134,13 @@ async fn create_tray_menu(
],
)?;
let quit = &MenuItem::with_id(app_handle, "quit", exit_text, true, Some("CmdOrControl+Q"))?;
let quit = &MenuItem::with_id(
app_handle,
MenuIds::EXIT,
&texts.exit,
true,
Some("CmdOrControl+Q"),
)?;
let separator = &PredefinedMenuItem::separator(app_handle)?;
@@ -1063,9 +1158,9 @@ async fn create_tray_menu(
// 如果有代理节点,添加代理节点菜单
if show_proxy_groups_inline {
if !inline_proxy_items.is_empty() {
menu_items.extend_from_slice(&inline_proxy_items);
menu_items.extend(inline_proxy_items.iter().map(|item| item.as_ref()));
}
} else if let Some(ref proxies_menu) = proxies_submenu {
} else if let Some(ref proxies_menu) = proxies_menu {
menu_items.push(proxies_menu);
}
@@ -1074,7 +1169,7 @@ async fn create_tray_menu(
system_proxy as &dyn IsMenuItem<Wry>,
tun_mode as &dyn IsMenuItem<Wry>,
separator,
lighteweight_mode as &dyn IsMenuItem<Wry>,
lightweight_mode as &dyn IsMenuItem<Wry>,
copy_env as &dyn IsMenuItem<Wry>,
open_dir as &dyn IsMenuItem<Wry>,
more as &dyn IsMenuItem<Wry>,
@@ -1091,13 +1186,14 @@ async fn create_tray_menu(
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
AsyncHandler::spawn(|| async move {
match event.id.as_ref() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5]; // Removing the "_mode" suffix
mode @ (MenuIds::RULE_MODE | MenuIds::GLOBAL_MODE | MenuIds::DIRECT_MODE) => {
// Removing the the "tray_" preffix and "_mode" suffix
let mode = &mode[5..mode.len() - 5];
logging!(info, Type::ProxyMode, "Switch Proxy Mode To: {}", mode);
feat::change_clash_mode(mode.into()).await;
}
"open_window" => {
log::info!(target: "app", "托盘菜单点击: 打开窗口");
MenuIds::DASHBOARD => {
logging!(info, Type::Tray, "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
@@ -1106,40 +1202,51 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
WindowManager::show_main_window().await;
};
}
"system_proxy" => {
MenuIds::SYSTEM_PROXY => {
feat::toggle_system_proxy().await;
}
"tun_mode" => {
MenuIds::TUN_MODE => {
feat::toggle_tun_mode(None).await;
}
"close_all_connections" => {
MenuIds::CLOSE_ALL_CONNECTIONS => {
if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await {
log::error!(target: "app", "Failed to close all connections from tray: {err}");
logging!(
error,
Type::Tray,
"Failed to close all connections from tray: {err}"
);
}
}
"copy_env" => feat::copy_clash_env().await,
"open_app_dir" => {
MenuIds::COPY_ENV => feat::copy_clash_env().await,
MenuIds::CONF_DIR => {
println!("Open directory submenu clicked");
let _ = cmd::open_app_dir().await;
}
"open_core_dir" => {
MenuIds::CORE_DIR => {
let _ = cmd::open_core_dir().await;
}
"open_logs_dir" => {
MenuIds::LOGS_DIR => {
let _ = cmd::open_logs_dir().await;
}
"restart_clash" => feat::restart_clash_core().await,
"restart_app" => feat::restart_app().await,
"entry_lightweight_mode" => {
MenuIds::APP_LOG => {
let _ = cmd::open_app_log().await;
}
MenuIds::CORE_LOG => {
let _ = cmd::open_core_log().await;
}
MenuIds::RESTART_CLASH => feat::restart_clash_core().await,
MenuIds::RESTART_APP => feat::restart_app().await,
MenuIds::LIGHTWEIGHT_MODE => {
if !should_handle_tray_click() {
return;
}
if !is_in_lightweight_mode() {
lightweight::entry_lightweight_mode().await; // Await async function
lightweight::entry_lightweight_mode().await;
} else {
lightweight::exit_lightweight_mode().await; // Await async function
lightweight::exit_lightweight_mode().await;
}
}
"quit" => {
MenuIds::EXIT => {
feat::quit().await;
}
id if id.starts_with("profiles_") => {
@@ -1160,12 +1267,25 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
.await
{
Ok(_) => {
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
logging!(
info,
Type::Tray,
"切换代理成功: {} -> {}",
group_name,
proxy_name
);
let _ = handle::Handle::app_handle()
.emit("verge://refresh-proxy-config", ());
}
Err(e) => {
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
e
);
// Fallback to IPC update
if (handle::Handle::mihomo()
@@ -1174,7 +1294,13 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
.await)
.is_ok()
{
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
logging!(
info,
Type::Tray,
"代理切换回退成功: {} -> {}",
group_name,
proxy_name
);
let app_handle = handle::Handle::app_handle();
let _ = app_handle.emit("verge://force-refresh-proxies", ());
@@ -1188,7 +1314,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
// Ensure tray state update is awaited and properly handled
if let Err(e) = Tray::global().update_all_states().await {
log::warn!(target: "app", "更新托盘状态失败: {e}");
logging!(warn, Type::Tray, "Failed to update tray state: {e}");
}
});
}

View File

@@ -1,9 +1,9 @@
use anyhow::Result;
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use tokio::fs;
use crate::config::{Config, ConfigType};
use crate::core::handle;
@@ -11,44 +11,38 @@ use crate::singleton_lazy;
use crate::utils::dirs;
use crate::{logging, utils::logging::Type};
// pub enum ValidationResult {
// Valid,
// Invalid(String),
// }
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum ValidationProcessStatus {
Ongoing,
Completed,
}
pub struct CoreConfigValidator {
// inner: Vec<String>,
// result: ValidationResult,
process_status: Arc<Mutex<ValidationProcessStatus>>,
is_processing: AtomicBool,
}
impl CoreConfigValidator {
pub fn new() -> Self {
CoreConfigValidator {
process_status: Arc::new(Mutex::new(ValidationProcessStatus::Completed)),
Self {
is_processing: AtomicBool::new(false),
}
}
pub fn try_start(&self) -> bool {
!self.is_processing.swap(true, Ordering::AcqRel)
}
pub fn finish(&self) {
self.is_processing.store(false, Ordering::Release)
}
}
impl CoreConfigValidator {
/// 检查文件是否为脚本文件
fn is_script_file<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!(
@@ -118,11 +112,11 @@ impl CoreConfigValidator {
}
/// 只进行文件语法检查,不进行完整验证
fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Validate, "开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
let content = match fs::read_to_string(config_path).await {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {err}").into();
@@ -147,9 +141,9 @@ impl CoreConfigValidator {
}
/// 验证脚本文件语法
fn validate_script_file(path: &str) -> Result<(bool, String)> {
async fn validate_script_file(path: &str) -> Result<(bool, String)> {
// 读取脚本内容
let content = match std::fs::read_to_string(path) {
let content = match fs::read_to_string(path).await {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {err}").into();
@@ -219,14 +213,14 @@ impl CoreConfigValidator {
"检测到Merge文件仅进行语法检查: {}",
config_path
);
return Self::validate_file_syntax(config_path);
return Self::validate_file_syntax(config_path).await;
}
// 检查是否为脚本文件
let is_script = if config_path.ends_with(".js") {
true
} else {
match Self::is_script_file(config_path) {
match Self::is_script_file(config_path).await {
Ok(result) => result,
Err(err) => {
// 如果无法确定文件类型尝试使用Clash内核验证
@@ -249,7 +243,7 @@ impl CoreConfigValidator {
"检测到脚本文件使用JavaScript验证: {}",
config_path
);
return Self::validate_script_file(config_path);
return Self::validate_script_file(config_path).await;
}
// 对YAML配置文件使用Clash内核验证
@@ -325,22 +319,18 @@ impl CoreConfigValidator {
/// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> {
if *self.process_status.lock().await == ValidationProcessStatus::Ongoing {
if !self.try_start() {
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
return Ok((true, String::new()));
}
*self.process_status.lock().await = ValidationProcessStatus::Ongoing;
defer! {
self.finish();
}
logging!(info, Type::Validate, "生成临时配置文件用于验证");
let result = async {
let config_path = Config::generate_file(ConfigType::Check).await?;
let config_path = dirs::path_to_str(&config_path)?;
Self::validate_config_internal(config_path).await
}
.await;
*self.process_status.lock().await = ValidationProcessStatus::Completed;
result
let config_path = Config::generate_file(ConfigType::Check).await?;
let config_path = dirs::path_to_str(&config_path)?;
Self::validate_config_internal(config_path).await
}
}

View File

@@ -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,

View File

@@ -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()
})
}

View File

@@ -6,17 +6,45 @@ pub mod seq;
mod tun;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::constants;
use crate::utils::dirs;
use crate::{config::Config, utils::tmpl};
use crate::{logging, utils::logging::Type};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tokio::fs;
type ResultLog = Vec<(String, String)>;
#[derive(Debug)]
struct ConfigValues {
clash_config: Mapping,
clash_core: Option<String>,
enable_tun: bool,
enable_builtin: bool,
socks_enabled: bool,
http_enabled: bool,
enable_dns_settings: bool,
#[cfg(not(target_os = "windows"))]
redir_enabled: bool,
#[cfg(target_os = "linux")]
tproxy_enabled: bool,
}
/// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
#[derive(Debug)]
struct ProfileItems {
config: Mapping,
merge_item: ChainItem,
script_item: ChainItem,
rules_item: ChainItem,
proxies_item: ChainItem,
groups_item: ChainItem,
global_merge: ChainItem,
global_script: ChainItem,
profile_name: String,
}
async fn get_config_values() -> ConfigValues {
let clash_config = { Config::clash().await.latest_ref().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
@@ -31,12 +59,14 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
verge.enable_dns_settings.unwrap_or(false),
)
};
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge().await;
@@ -44,9 +74,189 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
verge.verge_tproxy_enabled.unwrap_or(false)
};
ConfigValues {
clash_config,
clash_core,
enable_tun,
enable_builtin,
socks_enabled,
http_enabled,
enable_dns_settings,
#[cfg(not(target_os = "windows"))]
redir_enabled,
#[cfg(target_os = "linux")]
tproxy_enabled,
}
}
async fn collect_profile_items() -> ProfileItems {
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
let (
mut config,
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
_current_profile_uid,
name,
) = {
let current = {
let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_ref().clone();
profiles_clone.current_mapping().await.unwrap_or_default()
};
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
let script_uid = profiles_ref.current_script().unwrap_or_default();
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
let name = profiles_ref
.get_item(&current_profile_uid)
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_default();
(
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
current_profile_uid,
name,
)
};
// 现在获取具体的items此时profiles锁已经释放
let merge_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(merge_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
});
let script_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(script_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let rules_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(rules_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
});
let proxies_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(proxies_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
});
let groups_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(groups_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
});
let global_merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item("Merge").ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
});
let global_script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item("Script").ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
ProfileItems {
config: current,
merge_item,
script_item,
rules_item,
@@ -54,192 +264,19 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups_item,
global_merge,
global_script,
profile_name,
) = {
// 收集所有需要的数据然后释放profiles锁
let (
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
_current_profile_uid,
name,
) = {
// 分离async调用和数据获取避免借用检查问题
let current = {
let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_ref().clone();
profiles_clone.current_mapping().await.unwrap_or_default()
};
profile_name: name,
}
}
// 重新获取锁进行其他操作
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
fn process_global_items(
mut config: Mapping,
global_merge: ChainItem,
global_script: ChainItem,
profile_name: String,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
let mut result_map = HashMap::new();
let mut exists_keys = use_keys(&config);
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
let script_uid = profiles_ref.current_script().unwrap_or_default();
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
let name = profiles_ref
.get_item(&current_profile_uid)
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_default();
(
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
current_profile_uid,
name,
)
};
// 现在获取具体的items此时profiles锁已经释放
let merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&merge_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
});
let script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&script_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let rules = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&rules_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
});
let proxies = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&proxies_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
});
let groups = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&groups_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
});
let global_merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&"Merge".into()).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
});
let global_script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&"Script".into()).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
(
current,
merge,
script,
rules,
proxies,
groups,
global_merge,
global_script,
name,
)
};
let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys
// 全局Merge和Script
if let ChainType::Merge(merge) = global_merge.data {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
@@ -247,7 +284,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, config.to_owned(), profile_name.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
@@ -256,11 +292,24 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}
Err(err) => logs.push(("exception".into(), err.to_string().into())),
}
result_map.insert(global_script.uid, logs);
}
// 订阅关联的Merge、Script、Rules、Proxies、Groups
(config, exists_keys, result_map)
}
#[allow(clippy::too_many_arguments)]
fn process_profile_items(
mut config: Mapping,
mut exists_keys: Vec<String>,
mut result_map: HashMap<String, ResultLog>,
rules_item: ChainItem,
proxies_item: ChainItem,
groups_item: ChainItem,
merge_item: ChainItem,
script_item: ChainItem,
profile_name: String,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Rules(rules) = rules_item.data {
config = use_seq(rules, config.to_owned(), "rules");
}
@@ -280,7 +329,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Script(script) = script_item.data {
let mut logs = vec![];
match use_script(script, config.to_owned(), profile_name) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
@@ -289,11 +337,20 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}
Err(err) => logs.push(("exception".into(), err.to_string().into())),
}
result_map.insert(script_item.uid, logs);
}
// 合并默认的config
(config, exists_keys, result_map)
}
async fn merge_default_config(
mut config: Mapping,
clash_config: Mapping,
socks_enabled: bool,
http_enabled: bool,
#[cfg(not(target_os = "windows"))] redir_enabled: bool,
#[cfg(target_os = "linux")] tproxy_enabled: bool,
) -> Mapping {
for (key, value) in clash_config.into_iter() {
if key.as_str() == Some("tun") {
let mut tun = config.get_mut("tun").map_or_else(Mapping::new, |val| {
@@ -353,66 +410,140 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}
}
// 内建脚本最后跑
config
}
fn apply_builtin_scripts(
mut config: Mapping,
clash_core: Option<String>,
enable_builtin: bool,
) -> Mapping {
if enable_builtin {
ChainItem::builtin()
.into_iter()
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c)
.for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid);
logging!(debug, Type::Core, "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned(), "".into()) {
Ok((res_config, _)) => {
config = res_config;
}
Err(err) => {
log::error!(target: "app", "builtin script error `{err}`");
logging!(error, Type::Core, "builtin script error `{err}`");
}
}
}
});
}
config = use_tun(config, enable_tun);
config = use_sort(config);
config
}
// 应用独立的DNS配置如果启用
if enable_dns_settings {
use crate::utils::dirs;
use std::fs;
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path)
&& let Ok(dns_config) = serde_yaml_ng::from_str::<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()
{
// 处理hosts配置
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
{
config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration");
}
config.insert("hosts".into(), hosts_value.clone());
logging!(info, Type::Core, "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
logging!(info, Type::Core, "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
logging!(info, Type::Core, "apply dns_config.yaml");
}
}
}
config
}
/// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// gather config values
let cfg_vals = get_config_values().await;
let ConfigValues {
clash_config,
clash_core,
enable_tun,
enable_builtin,
socks_enabled,
http_enabled,
enable_dns_settings,
#[cfg(not(target_os = "windows"))]
redir_enabled,
#[cfg(target_os = "linux")]
tproxy_enabled,
} = cfg_vals;
// collect profile items
let profile = collect_profile_items().await;
let config = profile.config;
let merge_item = profile.merge_item;
let script_item = profile.script_item;
let rules_item = profile.rules_item;
let proxies_item = profile.proxies_item;
let groups_item = profile.groups_item;
let global_merge = profile.global_merge;
let global_script = profile.global_script;
let profile_name = profile.profile_name;
// process globals
let (config, exists_keys, result_map) =
process_global_items(config, global_merge, global_script, profile_name.clone());
// process profile-specific items
let (config, exists_keys, result_map) = process_profile_items(
config,
exists_keys,
result_map,
rules_item,
proxies_item,
groups_item,
merge_item,
script_item,
profile_name,
);
// merge default clash config
let config = merge_default_config(
config,
clash_config,
socks_enabled,
http_enabled,
#[cfg(not(target_os = "windows"))]
redir_enabled,
#[cfg(target_os = "linux")]
tproxy_enabled,
)
.await;
// builtin scripts
let mut config = apply_builtin_scripts(config, clash_core, enable_builtin);
config = use_tun(config, enable_tun);
config = use_sort(config);
// dns settings
config = apply_dns_settings(config, enable_dns_settings).await;
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys);
exists_keys = exists_set.into_iter().collect();
let exists_keys: Vec<String> = exists_set.into_iter().collect();
(config, exists_keys, result_map)
}

View File

@@ -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"
);
}
}

View File

@@ -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())
@@ -239,12 +247,13 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let mut zip = zip::ZipArchive::new(fs::File::open(&target_path)?)?;
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
let mut zip = zip::ZipArchive::new(file)?;
zip.extract(app_home_dir()?)?;
logging_error!(
Type::Backup,
super::patch_verge(
IVerge {
&IVerge {
webdav_url,
webdav_username,
webdav_password,
@@ -258,7 +267,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
}
/// Export local backup file to user selected destination
pub fn export_local_backup(filename: String, destination: String) -> Result<()> {
pub async fn export_local_backup(filename: String, destination: String) -> Result<()> {
let backup_dir = local_backup_dir()?;
let source_path = backup_dir.join(filename.as_str());
if !source_path.exists() {
@@ -267,10 +276,11 @@ pub fn export_local_backup(filename: String, destination: String) -> Result<()>
let dest_path = PathBuf::from(destination.as_str());
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
fs::create_dir_all(parent).await?;
}
fs::copy(&source_path, &dest_path)
.await
.map(|_| ())
.map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?;
Ok(())

View File

@@ -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)
}
}

View File

@@ -63,23 +63,19 @@ enum UpdateFlags {
LighteWeight = 1 << 10,
}
/// Patch Verge configuration
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
fn determine_update_flags(patch: &IVerge) -> i32 {
let mut update_flags: i32 = UpdateFlags::None as i32;
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
let pac = patch.proxy_auto_config;
let pac_content = patch.pac_file_content;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let pac_content = &patch.pac_file_content;
let proxy_bypass = &patch.system_proxy_bypass;
let language = &patch.language;
let mixed_port = patch.verge_mixed_port;
#[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon;
let tray_icon = &patch.tray_icon;
#[cfg(not(target_os = "macos"))]
let tray_icon: Option<String> = None;
let common_tray_icon = patch.common_tray_icon;
@@ -98,147 +94,156 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let enable_tray_speed = patch.enable_tray_speed;
let enable_tray_icon = patch.enable_tray_icon;
// let enable_tray_icon = patch.enable_tray_icon;
let enable_global_hotkey = patch.enable_global_hotkey;
let tray_event = patch.tray_event;
let tray_event = &patch.tray_event;
let home_cards = patch.home_cards.clone();
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
let enable_external_controller = patch.enable_external_controller;
let res: std::result::Result<(), anyhow::Error> = {
// Initialize with no flags set
let mut update_flags: i32 = UpdateFlags::None as i32;
let tray_inline_proxy_groups = patch.tray_inline_proxy_groups;
if tun_mode.is_some() {
update_flags |= UpdateFlags::ClashConfig as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
update_flags |= UpdateFlags::SystrayTooltip as i32;
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if enable_global_hotkey.is_some() || home_cards.is_some() {
update_flags |= UpdateFlags::VergeConfig as i32;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() || redir_port.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() || tproxy_port.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
if socks_enabled.is_some()
|| http_enabled.is_some()
|| socks_port.is_some()
|| http_port.is_some()
|| mixed_port.is_some()
{
update_flags |= UpdateFlags::RestartCore as i32;
}
if auto_launch.is_some() {
update_flags |= UpdateFlags::Launch as i32;
}
if tun_mode.is_some() {
update_flags |= UpdateFlags::ClashConfig as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
update_flags |= UpdateFlags::SystrayTooltip as i32;
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if enable_global_hotkey.is_some() || home_cards.is_some() {
update_flags |= UpdateFlags::VergeConfig as i32;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() || redir_port.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() || tproxy_port.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
if socks_enabled.is_some()
|| http_enabled.is_some()
|| socks_port.is_some()
|| http_port.is_some()
|| mixed_port.is_some()
{
update_flags |= UpdateFlags::RestartCore as i32;
}
if auto_launch.is_some() {
update_flags |= UpdateFlags::Launch as i32;
}
if system_proxy.is_some() {
update_flags |= UpdateFlags::SysProxy as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
update_flags |= UpdateFlags::SystrayTooltip as i32;
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if system_proxy.is_some() {
update_flags |= UpdateFlags::SysProxy as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
update_flags |= UpdateFlags::SystrayTooltip as i32;
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
update_flags |= UpdateFlags::SysProxy as i32;
}
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
update_flags |= UpdateFlags::SysProxy as i32;
}
if language.is_some() {
update_flags |= UpdateFlags::SystrayMenu as i32;
}
if common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
|| tray_icon.is_some()
|| enable_tray_speed.is_some()
|| enable_tray_icon.is_some()
{
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if language.is_some() {
update_flags |= UpdateFlags::SystrayMenu as i32;
}
if common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
|| tray_icon.is_some()
|| enable_tray_speed.is_some()
// || enable_tray_icon.is_some()
{
update_flags |= UpdateFlags::SystrayIcon as i32;
}
if patch.hotkeys.is_some() {
update_flags |= UpdateFlags::Hotkey as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
}
if patch.hotkeys.is_some() {
update_flags |= UpdateFlags::Hotkey as i32;
update_flags |= UpdateFlags::SystrayMenu as i32;
}
if tray_event.is_some() {
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
}
if tray_event.is_some() {
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
}
if enable_auto_light_weight.is_some() {
update_flags |= UpdateFlags::LighteWeight as i32;
}
if enable_auto_light_weight.is_some() {
update_flags |= UpdateFlags::LighteWeight as i32;
}
// 处理 external-controller 的开关
if enable_external_controller.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
if enable_external_controller.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
// Process updates based on flags
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
Config::generate().await?;
CoreManager::global().restart_core().await?;
}
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
CoreManager::global().update_config().await?;
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey;
handle::Handle::refresh_verge();
}
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
sysopt::Sysopt::global().update_launch().await?;
}
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
&& let Some(hotkeys) = patch.hotkeys
{
hotkey::Hotkey::global().update(hotkeys).await?;
}
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu().await?;
}
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
tray::Tray::global().update_icon().await?;
}
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
tray::Tray::global().update_tooltip().await?;
}
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
tray::Tray::global().update_click_behavior().await?;
}
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
if enable_auto_light_weight.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode().await;
} else {
lightweight::disable_auto_light_weight_mode();
}
}
if tray_inline_proxy_groups.is_some() {
update_flags |= UpdateFlags::SystrayMenu as i32;
}
<Result<()>>::Ok(())
};
match res {
Ok(()) => {
Config::verge().await.apply();
if !not_save_file {
// 分离数据获取和异步调用
let verge_data = Config::verge().await.data_mut().clone();
verge_data.save_file().await?;
}
update_flags
}
Ok(())
}
Err(err) => {
Config::verge().await.discard();
Err(err)
async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<()> {
// Process updates based on flags
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
Config::generate().await?;
CoreManager::global().restart_core().await?;
}
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
CoreManager::global().update_config().await?;
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey;
handle::Handle::refresh_verge();
}
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
sysopt::Sysopt::global().update_launch().await?;
}
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
&& let Some(hotkeys) = &patch.hotkeys
{
hotkey::Hotkey::global().update(hotkeys.to_owned()).await?;
}
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu().await?;
}
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
tray::Tray::global().update_icon().await?;
}
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
tray::Tray::global().update_tooltip().await?;
}
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
tray::Tray::global().update_click_behavior().await?;
}
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
if patch.enable_auto_light_weight_mode.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode().await;
} else {
lightweight::disable_auto_light_weight_mode();
}
}
Ok(())
}
pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.draft_mut().patch_config(patch);
let update_flags = determine_update_flags(patch);
let process_flag_result: std::result::Result<(), anyhow::Error> = {
process_terminated_flags(update_flags, patch).await?;
Ok(())
};
if let Err(err) = process_flag_result {
Config::verge().await.discard();
return Err(err);
}
Config::verge().await.apply();
if !not_save_file {
// 分离数据获取和异步调用
let verge_data = Config::verge().await.data_ref().clone();
verge_data.save_file().await?;
}
Ok(())
}

View File

@@ -18,154 +18,188 @@ pub async fn toggle_proxy_profile(profile_index: String) {
}
}
Err(err) => {
log::error!(target: "app", "{err}");
logging!(error, Type::Tray, "{err}");
}
}
}
/// Update a profile
/// If updating current profile, activate it
/// auto_refresh: 是否自动更新配置和刷新前端
pub async fn update_profile(
uid: String,
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性
async fn should_update_profile(
uid: &String,
ignore_auto_update: bool,
) -> Result<Option<(String, Option<PrfOption>)>> {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let item = profiles.get_item(uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
let url_opt = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
None // 非远程订阅直接更新
} else if item.url.is_none() {
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL无法更新");
bail!("failed to get the profile item url");
} else if !item
if !is_remote {
logging!(
info,
Type::Config,
"[订阅更新] {uid} 不是远程订阅,跳过更新"
);
Ok(None)
} else if item.url.is_none() {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] {uid} 缺少URL无法更新"
);
bail!("failed to get the profile item url");
} else if !ignore_auto_update
&& !item
.option
.as_ref()
.and_then(|o| o.allow_auto_update)
.unwrap_or(true)
{
log::info!(target: "app", "[订阅更新] {} 禁止自动更新,跳过更新", uid);
None
} else {
log::info!(target: "app",
"[订阅更新] {} 是远程订阅URL: {}",
uid,
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
);
Some((
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
item.option.clone(),
))
}
{
logging!(
info,
Type::Config,
"[订阅更新] {} 禁止自动更新,跳过更新",
uid
);
Ok(None)
} else {
logging!(
info,
Type::Config,
"[订阅更新] {} 是远程订阅URL: {}",
uid,
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
);
Ok(Some((
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
item.option.clone(),
)))
}
}
async fn perform_profile_update(
uid: &String,
url: &String,
opt: Option<&PrfOption>,
option: Option<&PrfOption>,
) -> Result<bool> {
logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容");
let mut merged_opt = PrfOption::merge(opt, option);
let is_current = {
let profiles = Config::profiles().await;
profiles.latest_ref().is_current_profile_index(uid)
};
let profile_name = {
let profiles = Config::profiles().await;
profiles
.latest_ref()
.get_name_by_uid(uid)
.unwrap_or_default()
};
let mut last_err;
let should_update = match url_opt {
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功");
profiles_draft_update_item_safe(uid, &mut item).await?;
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err}尝试使用Clash代理更新"
);
last_err = err;
}
}
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(true);
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(false);
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(
info,
Type::Config,
"[订阅更新] 使用 Clash代理 更新订阅配置成功"
);
profiles_draft_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err}尝试使用Clash代理更新"
);
last_err = err;
}
}
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(false);
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(true);
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(
info,
Type::Config,
"[订阅更新] 使用 系统代理 更新订阅配置成功"
);
profiles_draft_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err},尝试使用系统代理更新"
);
last_err = err;
}
}
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{profile_name} - {last_err}"),
);
Ok(is_current)
}
pub async fn update_profile(
uid: &String,
option: Option<&PrfOption>,
auto_refresh: bool,
ignore_auto_update: bool,
) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let url_opt = should_update_profile(uid, ignore_auto_update).await?;
let should_refresh = match url_opt {
Some((url, opt)) => {
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles().await;
// 使用Send-safe helper函数
let result = profiles_draft_update_item_safe(uid.clone(), item).await;
result?;
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(err) => {
// 首次更新失败尝试使用Clash代理
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err}尝试使用Clash代理更新");
// 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
// 保存原始代理设置
let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy);
let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy);
// 创建使用Clash代理的选项
let mut fallback_opt = merged_opt.unwrap_or_default();
fallback_opt.with_proxy = Some(false);
fallback_opt.self_proxy = Some(true);
// 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => {
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
// 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() {
option.with_proxy = original_with_proxy;
option.self_proxy = original_self_proxy;
}
// 更新到配置
let profiles = Config::profiles().await;
// 使用 Send-safe 方法进行数据操作
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
// 获取配置名称用于通知
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
// 发送通知告知用户自动更新使用了回退机制
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(retry_err) => {
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{retry_err}"),
);
return Err(retry_err);
}
}
}
}
perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh
}
None => auto_refresh,
};
if should_update {
if should_refresh {
logging!(info, Type::Config, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
logging!(info, Type::Config, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
// if let Err(err) = cmd::proxy::force_refresh_proxies().await {
// logging!(
// error,
// Type::Config,
// true,
// "[订阅更新] 代理组刷新失败: {}",
// err
// );
// }
}
Err(err) => {
logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}");
logging!(error, Type::Config, "{err}");
}
}
}

View File

@@ -1,35 +1,32 @@
use crate::{
config::{Config, IVerge},
core::handle,
logging,
utils::logging::Type,
};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off
pub async fn toggle_system_proxy() {
// 获取当前系统代理状态
let enable = {
let verge = Config::verge().await;
verge.latest_ref().enable_system_proxy.unwrap_or(false)
};
// 获取自动关闭连接设置
let auto_close_connection = {
let verge = Config::verge().await;
verge.latest_ref().auto_close_connection.unwrap_or(false)
};
let verge = Config::verge().await;
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false);
// 如果当前系统代理即将关闭且自动关闭连接设置为true则关闭所有连接
if enable
&& auto_close_connection
&& let Err(err) = handle::Handle::mihomo().await.close_all_connections().await
{
log::error!(target: "app", "Failed to close all connections: {err}");
logging!(
error,
Type::ProxyMode,
"Failed to close all connections: {err}"
);
}
let patch_result = super::patch_verge(
IVerge {
&IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
},
@@ -39,7 +36,7 @@ pub async fn toggle_system_proxy() {
match patch_result {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
}
}
@@ -49,7 +46,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = enable.unwrap_or(false);
match super::patch_verge(
IVerge {
&IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
},
@@ -58,7 +55,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
}
}
@@ -113,12 +110,16 @@ pub async fn copy_clash_env() {
}
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
_ => {
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
logging!(
error,
Type::ProxyMode,
"copy_clash_env: Invalid env type! {env_type}"
);
return;
}
};
if cliboard.write_text(export_text).is_err() {
log::error!(target: "app", "Failed to write to clipboard");
logging!(error, Type::ProxyMode, "Failed to write to clipboard");
}
}

View File

@@ -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
}
}

View File

@@ -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,
@@ -285,6 +288,11 @@ pub fn run() {
pub async fn handle_reopen(has_visible_windows: bool) {
handle::Handle::global().init();
if lightweight::is_in_lightweight_mode() {
lightweight::exit_lightweight_mode().await;
return;
}
if !has_visible_windows {
handle::Handle::global().set_activation_policy_regular();
let _ = WindowManager::show_main_window().await;

View File

@@ -10,6 +10,5 @@ fn main() {
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
}
}
app_lib::run();
}

View File

@@ -1,6 +1,6 @@
use crate::{
config::Config,
core::{handle, timer::Timer, tray::Tray},
core::{handle, timer::Timer},
log_err, logging,
process::AsyncHandler,
utils::logging::Type,
@@ -43,49 +43,42 @@ impl LightweightState {
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
fn set_state(new: LightweightState) {
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
match new {
LightweightState::Normal => {
logging!(info, Type::Lightweight, "轻量模式已关闭");
}
LightweightState::In => {
logging!(info, Type::Lightweight, "轻量模式已开启");
}
LightweightState::Exiting => {
logging!(info, Type::Lightweight, "正在退出轻量模式");
}
}
}
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
#[inline]
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
// 检查是否处于轻量模式
#[inline]
fn try_transition(from: LightweightState, to: LightweightState) -> bool {
LIGHTWEIGHT_STATE
.compare_exchange(
from.as_u8(),
to.as_u8(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}
#[inline]
fn record_state_and_log(state: LightweightState) {
LIGHTWEIGHT_STATE.store(state.as_u8(), Ordering::Release);
match state {
LightweightState::Normal => logging!(info, Type::Lightweight, "轻量模式已关闭"),
LightweightState::In => logging!(info, Type::Lightweight, "轻量模式已开启"),
LightweightState::Exiting => logging!(info, Type::Lightweight, "正在退出轻量模式"),
}
}
#[inline]
pub fn is_in_lightweight_mode() -> bool {
get_state() == LightweightState::In
}
// 设置轻量模式状态(仅 Normal <-> In
async fn set_lightweight_mode(value: bool) {
let current = get_state();
if value && current != LightweightState::In {
set_state(LightweightState::In);
} else if !value && current != LightweightState::Normal {
set_state(LightweightState::Normal);
}
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
pub async fn run_once_auto_lightweight() {
pub async fn auto_lightweight_boot() -> Result<()> {
let verge_config = Config::verge().await;
let enable_auto = verge_config
.data_mut()
@@ -96,39 +89,23 @@ pub async fn run_once_auto_lightweight() {
.enable_silent_start
.unwrap_or(false);
if !(enable_auto && is_silent_start) {
logging!(
info,
Type::Lightweight,
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
);
return;
if is_silent_start {
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式");
let _ = entry_lightweight_mode().await;
return Ok(());
}
set_lightweight_mode(true).await;
if !enable_auto {
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
return Ok(());
}
logging!(
info,
Type::Lightweight,
"非静默启动:注册自动轻量模式监听器"
);
enable_auto_light_weight_mode().await;
}
pub async fn auto_lightweight_mode_init() -> Result<()> {
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto = {
Config::verge()
.await
.latest_ref()
.enable_auto_light_weight_mode
}
.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await;
}
Ok(())
}
@@ -151,43 +128,18 @@ pub fn disable_auto_light_weight_mode() {
}
pub async fn entry_lightweight_mode() -> bool {
// 尝试从 Normal -> In
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::Normal as u8,
LightweightState::In as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
return false;
}
record_state_and_log(LightweightState::In);
WindowManager::destroy_main_window();
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer();
// 回到 In
set_state(LightweightState::In);
true
}
// 添加从轻量模式恢复的函数
pub async fn exit_lightweight_mode() -> bool {
// 尝试从 In -> Exiting
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::In as u8,
LightweightState::Exiting as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
if !try_transition(LightweightState::In, LightweightState::Exiting) {
logging!(
info,
Type::Lightweight,
@@ -195,16 +147,10 @@ pub async fn exit_lightweight_mode() -> bool {
);
return false;
}
record_state_and_log(LightweightState::Exiting);
WindowManager::show_main_window().await;
set_lightweight_mode(false).await;
let _ = cancel_light_weight_timer();
// 回到 Normal
set_state(LightweightState::Normal);
logging!(info, Type::Lightweight, "轻量模式退出完成");
record_state_and_log(LightweightState::Normal);
true
}
@@ -215,24 +161,31 @@ pub async fn add_light_weight_timer() {
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = window.listen("tauri://close-requested", move |_event| {
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
if let Err(e) = setup_light_weight_timer().await {
log::warn!("Failed to setup light weight timer: {e}");
logging!(
warn,
Type::Lightweight,
"Warning: Failed to setup light weight timer: {e}"
);
}
}));
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
});
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
}
}
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
}
}
@@ -240,7 +193,11 @@ fn cancel_window_close_listener() {
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = window.listen("tauri://focus", move |_event| {
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
logging!(
info,
@@ -248,37 +205,45 @@ fn setup_webview_focus_listener() {
"监听到窗口获得焦点,取消轻量模式计时"
);
});
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
}
}
fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
}
}
}
async fn setup_light_weight_timer() -> Result<()> {
Timer::global().init().await?;
if let Err(e) = Timer::global().init().await {
return Err(e).context("failed to initialize timer");
}
let once_by_minutes = Config::verge()
.await
.latest_ref()
.auto_light_weight_minutes
.unwrap_or(10);
// 获取task_id
{
let timer_map = Timer::global().timer_map.read();
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建");
return Ok(());
}
}
let task_id = {
Timer::global()
.timer_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
};
// 创建任务
let task = TaskBuilder::default()
.set_task_id(task_id)
.set_maximum_parallel_runnable_num(1)
@@ -289,7 +254,6 @@ async fn setup_light_weight_timer() -> Result<()> {
})
.context("failed to create timer task")?;
// 添加任务到定时器
{
let delay_timer = Timer::global().delay_timer.write();
delay_timer
@@ -297,7 +261,6 @@ async fn setup_light_weight_timer() -> Result<()> {
.context("failed to add timer task")?;
}
// 更新任务映射
{
let mut timer_map = Timer::global().timer_map.write();
let timer_task = crate::core::timer::TimerTask {

View File

@@ -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
);
}
}
}

View File

@@ -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(())

View File

@@ -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
}
})

View File

@@ -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));

View File

@@ -1,11 +1,18 @@
use crate::{config::Config, utils::dirs};
use once_cell::sync::Lazy;
use serde_json::Value;
use std::{fs, path::PathBuf, sync::RwLock};
use smartstring::alias::String;
use std::{
collections::HashMap,
fs,
path::PathBuf,
sync::{Arc, RwLock},
};
use sys_locale;
const DEFAULT_LANGUAGE: &str = "zh";
type TranslationMap = (String, HashMap<String, Arc<str>>);
fn get_locales_dir() -> Option<PathBuf> {
dirs::app_resources_dir()
.map(|resource_path| resource_path.join("locales"))
@@ -33,18 +40,33 @@ pub fn get_supported_languages() -> Vec<String> {
languages
}
static TRANSLATIONS: Lazy<RwLock<(String, Value)>> = Lazy::new(|| {
pub async fn current_language() -> String {
Config::verge()
.await
.latest_ref()
.language
.as_deref()
.map(String::from)
.unwrap_or_else(get_system_language)
}
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
let lang = get_system_language();
let json = load_lang_file(&lang).unwrap_or_else(|| Value::Object(Default::default()));
RwLock::new((lang, json))
let map = load_lang_file(&lang).unwrap_or_default();
RwLock::new((lang, map))
});
fn load_lang_file(lang: &str) -> Option<Value> {
fn load_lang_file(lang: &str) -> Option<HashMap<String, Arc<str>>> {
let locales_dir = get_locales_dir()?;
let file_path = locales_dir.join(format!("{lang}.json"));
fs::read_to_string(file_path)
.ok()
.and_then(|content| serde_json::from_str(&content).ok())
.and_then(|content| serde_json::from_str::<HashMap<String, String>>(&content).ok())
.map(|map| {
map.into_iter()
.map(|(k, v)| (k, Arc::from(v.as_str())))
.collect()
})
}
fn get_system_language() -> String {
@@ -55,44 +77,38 @@ fn get_system_language() -> String {
.unwrap_or_else(|| DEFAULT_LANGUAGE.into())
}
pub async fn t(key: &str) -> String {
let current_lang = Config::verge()
.await
.latest_ref()
.language
.as_deref()
.map(String::from)
.unwrap_or_else(get_system_language);
pub async fn t(key: &str) -> Arc<str> {
let current_lang = current_language().await;
{
if let Ok(cache) = TRANSLATIONS.read()
&& cache.0 == current_lang
&& let Some(text) = cache.1.get(key).and_then(|val| val.as_str())
&& let Some(text) = cache.1.get(key)
{
return text.into();
return Arc::clone(text);
}
}
if let Some(new_json) = load_lang_file(&current_lang)
if let Some(new_map) = load_lang_file(&current_lang)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (current_lang.clone(), new_json);
*cache = (current_lang.clone(), new_map);
if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) {
return text.into();
if let Some(text) = cache.1.get(key) {
return Arc::clone(text);
}
}
if current_lang != DEFAULT_LANGUAGE
&& let Some(default_json) = load_lang_file(DEFAULT_LANGUAGE)
&& let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (DEFAULT_LANGUAGE.into(), default_json);
*cache = (DEFAULT_LANGUAGE.into(), default_map);
if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) {
return text.into();
if let Some(text) = cache.1.get(key) {
return Arc::clone(text);
}
}
key.into()
Arc::from(key)
}

View File

@@ -3,6 +3,7 @@
use crate::utils::logging::NoModuleFilter;
use crate::{
config::*,
constants,
core::handle,
logging,
process::AsyncHandler,
@@ -304,7 +305,7 @@ async fn init_dns_config() -> Result<()> {
// 检查DNS配置文件是否存在
let app_dir = dirs::app_home_dir()?;
let dns_path = app_dir.join("dns_config.yaml");
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
if !dns_path.exists() {
logging!(info, Type::Setup, "Creating default DNS config file");
@@ -429,26 +430,8 @@ pub async fn init_resources() -> Result<()> {
let src_path = res_dir.join(file);
let dest_path = app_dir.join(file);
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src, &dest).await {
Ok(_) => {
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
}
};
};
if src_path.exists() && !dest_path.exists() {
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
handle_copy(&src_path, &dest_path, file).await;
continue;
}
@@ -458,12 +441,12 @@ pub async fn init_resources() -> Result<()> {
match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified {
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
handle_copy(&src_path, &dest_path, file).await;
}
}
_ => {
logging!(debug, Type::Setup, "failed to get modified '{}'", file);
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
handle_copy(&src_path, &dest_path, file).await;
}
};
}
@@ -563,3 +546,21 @@ pub async fn startup_script() -> Result<()> {
Ok(())
}
async fn handle_copy(src: &PathBuf, dest: &PathBuf, file: &str) {
match fs::copy(src, dest).await {
Ok(_) => {
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
}
};
}

View File

@@ -11,7 +11,7 @@ use tokio::sync::{Mutex, MutexGuard};
pub type SharedWriter = Arc<Mutex<FileLogWriter>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub enum Type {
Cmd,
Core,
@@ -29,7 +29,6 @@ pub enum Type {
Lightweight,
Network,
ProxyMode,
// Cache,
Validate,
ClashVergeRev,
}
@@ -53,7 +52,6 @@ impl fmt::Display for Type {
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
// Type::Cache => write!(f, "[Cache]"),
Type::Validate => write!(f, "[Validate]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
@@ -82,15 +80,6 @@ macro_rules! log_err {
};
}
#[macro_export]
macro_rules! trace_err {
($result: expr, $err_str: expr) => {
if let Err(err) = $result {
log::trace!(target: "app", "{}, err {}", $err_str, err);
}
}
}
/// wrap the anyhow error
/// transform the error to String
#[macro_export]

View File

@@ -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()?)
}
}

View File

@@ -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);
}
}
}

View File

@@ -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}");
}
}
}

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use smartstring::alias::String;
use crate::{
config::Config,
@@ -11,10 +10,7 @@ use crate::{
tray::Tray,
},
logging, logging_error,
module::{
lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
signal,
},
module::{lightweight::auto_lightweight_boot, signal},
process::AsyncHandler,
utils::{init, logging::Type, server, window_manager::WindowManager},
};
@@ -71,8 +67,7 @@ pub fn resolve_setup_async() {
tray_init,
init_timer(),
init_hotkey(),
init_auto_lightweight_mode(),
init_once_auto_lightweight(),
init_auto_lightweight_boot(),
);
});
}
@@ -103,7 +98,7 @@ pub(super) async fn resolve_setup_logger() {
logging_error!(Type::Setup, init::init_logger().await);
}
pub async fn resolve_scheme(param: String) -> Result<()> {
pub async fn resolve_scheme(param: &str) -> Result<()> {
logging_error!(Type::Setup, scheme::resolve_scheme(param).await);
Ok(())
}
@@ -128,12 +123,8 @@ pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, Hotkey::global().init().await);
}
pub(super) async fn init_once_auto_lightweight() {
run_once_auto_lightweight().await;
}
pub(super) async fn init_auto_lightweight_mode() {
logging_error!(Type::Setup, auto_lightweight_mode_init().await);
pub(super) async fn init_auto_lightweight_boot() {
logging_error!(Type::Setup, auto_lightweight_boot().await);
}
pub(super) fn init_signal() {

View File

@@ -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: {:?}",

View File

@@ -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()

View File

@@ -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()
{

View File

@@ -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
}

View File

@@ -506,7 +506,7 @@ export const CurrentProxyCard = () => {
// 导航到代理页面
const goToProxies = useCallback(() => {
navigate("/");
navigate("/proxies");
}, [navigate]);
// 获取要显示的代理节点

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -307,7 +307,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
)} */}
{OS === "macos" && (
{/* {OS === "macos" && (
<Item>
<ListItemText primary={t("Enable Tray Icon")} />
<GuardState
@@ -326,7 +326,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
<Switch edge="end" />
</GuardState>
</Item>
)}
)} */}
<Item>
<ListItemText primary={t("Show Proxy Groups Inline")} />
<GuardState

View File

@@ -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}

View File

@@ -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();
}
}
};

View File

@@ -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,
};
}

View File

@@ -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,

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -21,7 +21,6 @@
"Label-Connections": "الاتصالات",
"Label-Rules": "القواعد",
"Label-Logs": "السجلات",
"Label-Test": "اختبار",
"Label-Settings": "الإعدادات",
"Proxies": "الوكلاء",
"Proxy Groups": "مجموعات الوكلاء",
@@ -166,7 +165,6 @@
"Table View": "عرض الجدول",
"List View": "عرض القائمة",
"Close All": "إغلاق الكل",
"Default": "افتراضي",
"Download Speed": "سرعة التنزيل",
"Upload Speed": "سرعة الرفع",
"Host": "المضيف",
@@ -197,6 +195,8 @@
"Settings": "الإعدادات",
"System Setting": "إعدادات النظام",
"Tun Mode": "وضع TUN",
"TUN requires Service Mode": "يتطلب وضع TUN خدمة",
"Install Service": "تثبيت الخدمة ",
"Reset to Default": "إعادة تعيين إلى الافتراضي",
"Tun Mode Info": "وضع TUN (بطاقة شبكة افتراضية): يلتقط كل حركة المرور في النظام. عند تمكينه، لا حاجة لتفعيل وكيل النظام.",
"Stack": "مكدس TUN",
@@ -279,7 +279,8 @@
"Open UWP tool": "فتح أداة UWP",
"Open UWP tool Info": "منذ نظام ويندوز 8، يتم تقييد تطبيقات UWP من الوصول المباشر إلى المضيف المحلي. هذه الأداة تتيح تجاوز هذا التقييد",
"Update GeoData": "تحديث البيانات الجغرافية",
"Verge Setting": "إعدادات Verge",
"Verge Basic Setting": "الإعدادات الأساسية Verge",
"Verge Advanced Setting": "الإعدادات الأساسية Verge",
"Language": "اللغة",
"Theme Mode": "وضع السمة",
"theme.light": "سمة فاتحة",
@@ -365,6 +366,7 @@
"Profile Reactivated": "تم إعادة تنشيط الملف الشخصي",
"Only YAML Files Supported": "لا يتم دعم سوى ملفات YAML",
"Settings Applied": "تم تطبيق الإعدادات",
"Installing Service...": "جاري تثبيت الخدمة...",
"Service Installed Successfully": "تم تثبيت الخدمة بنجاح",
"Service Uninstalled Successfully": "تم إلغاء تثبيت الخدمة بنجاح",
"Proxy Daemon Duration Cannot be Less than 1 Second": "لا يمكن أن تقل مدة خادم الوكيل عن ثانية واحدة",
@@ -407,19 +409,12 @@
"Help": "مساعدة",
"About": "حول",
"Theme": "السمة",
"TUN Mode": "وضع TUN",
"Main Window": "النافذة الرئيسية",
"Group Icon": "أيقونة المجموعة",
"Menu Icon": "أيقونة القائمة",
"System Proxy Bypass": "تخطي وكيل النظام",
"PAC File": "ملف PAC",
"Web UI": "واجهة الويب",
"Hotkeys": "اختصارات لوحة المفاتيح",
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
"Enable Built-in Enhanced": "تفعيل التحسين المدمج",
"Proxy Layout Column": "عمود عرض الوكيل",
"Test List": "قائمة الاختبارات",
"Enable Random Port": "تفعيل المنفذ العشوائي",
"Verge Mixed Port": "منفذ Verge المختلط",
"Verge Socks Port": "منفذ Verge SOCKS",
"Verge Redir Port": "منفذ إعادة التوجيه لـ Verge",
@@ -429,15 +424,16 @@
"WebDAV URL": "رابط WebDAV",
"WebDAV Username": "اسم المستخدم لـ WebDAV",
"WebDAV Password": "كلمة مرور WebDAV",
"Dashboard": "لوحة التحكم",
"Restart App": "إعادة تشغيل التطبيق",
"Restart Clash Core": "إعادة تشغيل نواة Clash",
"TUN Mode": "وضع TUN",
"Copy Env": "نسخ البيئة",
"Conf Dir": "مجلد الإعدادات",
"Core Dir": "مجلد النواة",
"Logs Dir": "مجلد السجلات",
"Open Dir": "فتح المجلد",
"Restart Clash Core": "إعادة تشغيل نواة Clash",
"Restart App": "إعادة تشغيل التطبيق",
"More": "المزيد",
"Dashboard": "لوحة التحكم",
"Rule Mode": "وضع القواعد",
"Global Mode": "الوضع العالمي",
"Direct Mode": "الوضع المباشر",
@@ -454,10 +450,14 @@
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
"Core Changed Successfully": "تم تغيير النواة بنجاح",
"Failed to Change Core": "فشل تغيير النواة",
"Verge Basic Setting": "الإعدادات الأساسية Verge",
"Verge Advanced Setting": "الإعدادات الأساسية Verge",
"TUN requires Service Mode": "يتطلب وضع TUN خدمة",
"Install Service": ثبيت الخدمة ",
"Installing Service...": "جاري تثبيت الخدمة...",
"Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام"
"Service Administrator Prompt": "يتطلب Clash Verge امتيازات المسؤول لإعادة تثبيت خدمة النظام",
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
"Default": "افتراضي",
"Enable Built-in Enhanced": فعيل التحسين المدمج",
"Enable Random Port": "تفعيل المنفذ العشوائي",
"Label-Test": "اختبار",
"Proxy Layout Column": "عمود عرض الوكيل",
"System Proxy Bypass": "تخطي وكيل النظام",
"Test List": "قائمة الاختبارات",
"Verge Setting": "إعدادات Verge"
}

View File

@@ -195,10 +195,11 @@
"Settings": "Einstellungen",
"System Setting": "Systemeinstellungen",
"Tun Mode": "Virtual Network Interface-Modus",
"TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus",
"Install Service": "Service installieren",
"Uninstall Service": "Dienst deinstallieren",
"Reset to Default": "Auf Standardwerte zurücksetzen",
"Tun Mode Info": "Der TUN-Modus (Virtual Network Interface) übernimmt den gesamten Systemverkehr. Wenn dieser Modus aktiviert ist, muss der Systemproxy nicht geöffnet werden.",
"TUN requires Service Mode or Admin Mode": "TUN-Modus erfordert Service-Modus oder Administrator-Modus",
"System Proxy Enabled": "Der Systemproxy ist aktiviert. Ihre Anwendungen werden über den Proxy auf das Netzwerk zugreifen.",
"System Proxy Disabled": "Der Systemproxy ist deaktiviert. Es wird empfohlen, diesen Eintrag für die meisten Benutzer zu aktivieren.",
"TUN Mode Enabled": "Der TUN-Modus ist aktiviert. Die Anwendungen werden über die virtuelle Netzwerkschnittstelle auf das Netzwerk zugreifen.",
@@ -254,6 +255,7 @@
"Unified Delay Info": "Wenn die einheitliche Latenz aktiviert ist, werden zwei Latenztests durchgeführt, um die Latenzunterschiede zwischen verschiedenen Knotentypen aufgrund von Verbindungsaufbau und anderen Faktoren zu eliminieren.",
"Log Level": "Protokolliergrad",
"Log Level Info": "Dies wirkt sich nur auf die Kernprotokolldateien im Verzeichnis Service im Protokollverzeichnis aus.",
"Port Config": "Port-Konfiguration",
"Random Port": "Zufälliger Port",
"Mixed Port": "Mischter Proxy-Port",
"Socks Port": "SOCKS-Proxy-Port",
@@ -367,17 +369,16 @@
"Stopping Core...": "Kern wird gestoppt...",
"Restarting Core...": "Kern wird neu gestartet...",
"Installing Service...": "Service wird installiert...",
"Uninstall Service": "Dienst deinstallieren",
"Service Installed Successfully": "Service erfolgreich installiert",
"Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet",
"Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.",
"Uninstalling Service...": "Service wird deinstalliert...",
"Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...",
"Service Installed Successfully": "Service erfolgreich installiert",
"Service Uninstalled Successfully": "Service erfolgreich deinstalliert",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Das Intervall des Proxy-Daemons darf nicht weniger als 1 Sekunde betragen.",
"Invalid Bypass Format": "Ungültiges Format für die Proxy-Umgehung",
"Waiting for service to be ready...": "Auf Service-Bereitschaft gewartet...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "Der Dienst war bereit, aber beim Neustart des Kerns könnten Probleme aufgetreten sein oder der Dienst ist möglicherweise nicht verfügbar. Bitte überprüfen Sie dies.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Bei der Dienstinstallation oder dem Neustart des Kerns sind Probleme aufgetreten. Der Dienst ist möglicherweise nicht verfügbar. Bitte prüfen Sie die Systemprotokolle.",
"Service is ready and core restarted": "Service ist bereit und Kern wurde neu gestartet",
"Core restarted. Service is now available.": "Kern wurde neu gestartet. Service ist jetzt verfügbar",
"Core Version Updated": "Kernversion wurde aktualisiert",
"Clash Core Restarted": "Clash-Kern wurde neu gestartet",
"GeoData Updated": "Geo-Daten wurden aktualisiert",
@@ -523,7 +524,6 @@
"Unknown": "Unbekannt",
"Auto update disabled": "Automatische Aktualisierung deaktiviert",
"Update subscription successfully": "Abonnement erfolgreich aktualisiert",
"Update failed, retrying with Clash proxy...": "Abonnement-Aktualisierung fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
"Update with Clash proxy successfully": "Aktualisierung mit Clash-Proxy erfolgreich",
"Update failed even with Clash proxy": "Aktualisierung auch mit Clash-Proxy fehlgeschlagen",
"Profile creation failed, retrying with Clash proxy...": "Erstellung des Abonnements fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
@@ -555,10 +555,9 @@
"Disallowed ISP": "Nicht zugelassener Internetdienstanbieter",
"Originals Only": "Nur Original",
"Unsupported Country/Region": "Nicht unterstütztes Land/Region",
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
"Controller address copied to clipboard": "API-Port in die Zwischenablage kopiert",
"Secret copied to clipboard": "API-Schlüssel in die Zwischenablage kopiert",
"Copy to clipboard": "Klicken Sie hier, um zu kopieren",
"Port Config": "Port-Konfiguration",
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
"Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren"
}

View File

@@ -628,7 +628,6 @@
"Unknown": "Unknown",
"Auto update disabled": "Auto update disabled",
"Update subscription successfully": "Update subscription successfully",
"Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...",
"Update with Clash proxy successfully": "Update with Clash proxy successfully",
"Update failed even with Clash proxy": "Update failed even with Clash proxy",
"Profile creation failed, retrying with Clash proxy...": "Profile creation failed, retrying with Clash proxy...",
@@ -713,5 +712,7 @@
"Allow Auto Update": "Allow Auto Update",
"Menu reorder mode": "Menu reorder mode",
"Unlock menu order": "Unlock menu order",
"Lock menu order": "Lock menu order"
"Lock menu order": "Lock menu order",
"Open App Log": "Open App Log",
"Open Core Log": "Open Core Log"
}

View File

@@ -195,10 +195,11 @@
"Settings": "Ajustes",
"System Setting": "Ajustes del sistema",
"Tun Mode": "Modo de interfaz virtual (TUN)",
"TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador",
"Install Service": "Instalar servicio",
"Uninstall Service": "Desinstalar servicio",
"Reset to Default": "Restablecer a los valores predeterminados",
"Tun Mode Info": "El modo TUN (interfaz virtual) gestiona todo el tráfico del sistema. No es necesario habilitar el proxy del sistema cuando está activado.",
"TUN requires Service Mode or Admin Mode": "El modo TUN requiere el modo de servicio o el modo de administrador",
"System Proxy Enabled": "El proxy del sistema está habilitado. Sus aplicaciones accederán a Internet a través del proxy.",
"System Proxy Disabled": "El proxy del sistema está deshabilitado. Se recomienda a la mayoría de los usuarios habilitar esta opción.",
"TUN Mode Enabled": "El modo TUN está habilitado. Las aplicaciones accederán a Internet a través de la interfaz virtual.",
@@ -254,6 +255,7 @@
"Unified Delay Info": "Al habilitar la latencia unificada, se realizarán dos pruebas de latencia para eliminar las diferencias de latencia entre diferentes tipos de nodos causadas por el handshake de conexión, etc.",
"Log Level": "Nivel de registro",
"Log Level Info": "Solo se aplica al archivo de registro del núcleo en la carpeta Service del directorio de registros.",
"Port Config": "Configuración de puerto",
"Random Port": "Puerto aleatorio",
"Mixed Port": "Puerto de proxy mixto",
"Socks Port": "Puerto de proxy SOCKS",
@@ -367,17 +369,16 @@
"Stopping Core...": "Deteniendo núcleo...",
"Restarting Core...": "Reiniciando núcleo...",
"Installing Service...": "Instalando servicio...",
"Uninstall Service": "Desinstalar servicio",
"Service Installed Successfully": "Servicio instalado con éxito",
"Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado",
"Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.",
"Uninstalling Service...": "Desinstalando servicio...",
"Waiting for service to be ready...": "Esperando a que el servicio esté listo...",
"Service Installed Successfully": "Servicio instalado con éxito",
"Service Uninstalled Successfully": "Servicio desinstalado con éxito",
"Proxy Daemon Duration Cannot be Less than 1 Second": "El intervalo de tiempo del daemon de proxy no puede ser menor de 1 segundo",
"Invalid Bypass Format": "Formato de omisión de proxy no válido",
"Waiting for service to be ready...": "Esperando a que el servicio esté listo...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "El servicio estaba listo, pero puede haber habido problemas al reiniciar el núcleo o el servicio se volvió inaccesible. Por favor, verifique.",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "Hubo problemas durante la instalación del servicio o al reiniciar el núcleo. El servicio podría no estar disponible. Por favor, revise los registros del sistema.",
"Service is ready and core restarted": "El servicio está listo y el núcleo se ha reiniciado",
"Core restarted. Service is now available.": "El núcleo se ha reiniciado. El servicio está disponible.",
"Core Version Updated": "Versión del núcleo actualizada",
"Clash Core Restarted": "Núcleo de Clash reiniciado",
"GeoData Updated": "GeoData actualizado",
@@ -523,7 +524,6 @@
"Unknown": "Desconocido",
"Auto update disabled": "La actualización automática está deshabilitada",
"Update subscription successfully": "Suscripción actualizada con éxito",
"Update failed, retrying with Clash proxy...": "Error al actualizar la suscripción. Intentando con el proxy de Clash...",
"Update with Clash proxy successfully": "Actualización con el proxy de Clash exitosa",
"Update failed even with Clash proxy": "Error al actualizar incluso con el proxy de Clash",
"Profile creation failed, retrying with Clash proxy...": "Error al crear la suscripción. Intentando con el proxy de Clash...",
@@ -555,10 +555,9 @@
"Disallowed ISP": "Proveedor de servicios de Internet no permitido",
"Originals Only": "Solo originales",
"Unsupported Country/Region": "País/región no soportado",
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
"Controller address copied to clipboard": "El puerto API se copió al portapapeles",
"Secret copied to clipboard": "La clave API se copió al portapapeles",
"Copy to clipboard": "Haz clic aquí para copiar",
"Port Config": "Configuración de puerto",
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
"Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave"
}

View File

@@ -21,7 +21,6 @@
"Label-Connections": "اتصالات",
"Label-Rules": "قوانین",
"Label-Logs": "لاگ‌ها",
"Label-Test": "آزمون",
"Label-Settings": "تنظیمات",
"Proxies": "پراکسی‌ها",
"Proxy Groups": "گروه‌های پراکسی",
@@ -166,7 +165,6 @@
"Table View": "نمای جدولی",
"List View": "نمای لیستی",
"Close All": "بستن همه",
"Default": "پیش‌فرض",
"Download Speed": "سرعت دانلود",
"Upload Speed": "سرعت بارگذاری",
"Host": "میزبان",
@@ -197,6 +195,8 @@
"Settings": "تنظیمات",
"System Setting": "تنظیمات سیستم",
"Tun Mode": "Tun (کارت شبکه مجازی)",
"TUN requires Service Mode": "حالت تونل‌زنی نیاز به سرویس دارد",
"Install Service": "نصب سرویس",
"Reset to Default": "بازنشانی به پیش‌فرض",
"Tun Mode Info": "حالت Tun (NIC مجازی): تمام ترافیک سیستم را ضبط می کند، وقتی فعال باشد، نیازی به فعال کردن پروکسی سیستم نیست.",
"Stack": "انباشته Tun",
@@ -275,13 +275,13 @@
"Restart": "راه‌اندازی مجدد",
"Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا",
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید",
"Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
"Update GeoData": "به‌روزرسانی GeoData",
"Verge Setting": "تنظیمات Verge",
"Verge Basic Setting": "تنظیمات پایه Verge",
"Verge Advanced Setting": "تنظیمات پیشرفته Verge",
"Language": "زبان",
"Theme Mode": "حالت تم",
"theme.light": "روشن",
@@ -368,6 +368,7 @@
"Profile Reactivated": "پروفایل مجدداً فعال شد",
"Only YAML Files Supported": "فقط فایل‌های YAML پشتیبانی می‌شوند",
"Settings Applied": "تنظیمات اعمال شد",
"Installing Service...": "در حال نصب سرویس...",
"Service Installed Successfully": "سرویس با موفقیت نصب شد",
"Service Uninstalled Successfully": "سرویس با موفقیت حذف نصب شد",
"Proxy Daemon Duration Cannot be Less than 1 Second": "مدت زمان دیمن پراکسی نمی‌تواند کمتر از 1 ثانیه باشد",
@@ -451,10 +452,9 @@
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
"Core Changed Successfully": "هسته با موفقیت تغییر کرد",
"Failed to Change Core": "تغییر هسته ناموفق بود",
"Verge Basic Setting": "تنظیمات پایه Verge",
"Verge Advanced Setting": "تنظیمات پیشرفته Verge",
"TUN requires Service Mode": "حالت تونل‌زنی نیاز به سرویس دارد",
"Install Service": "نصب سرویس",
"Installing Service...": "در حال نصب سرویس...",
"Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد"
"Service Administrator Prompt": "Clash Verge برای نصب مجدد سرویس سیستم به امتیازات مدیر نیاز دارد",
"Default": "پیش‌فرض",
"Label-Test": "آزمون",
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Verge Setting": "تنظیمات Verge"
}

View File

@@ -21,40 +21,7 @@
"Label-Connections": "Koneksi",
"Label-Rules": "Aturan",
"Label-Logs": "Log",
"Label-Test": "Tes",
"Label-Settings": "Pengaturan",
"Dashboard": "Dasbor",
"Profile": "Profil",
"Help": "Bantuan",
"About": "Tentang",
"Theme": "Tema",
"Main Window": "Jendela Utama",
"Group Icon": "Ikon Grup",
"Menu Icon": "Ikon Menu",
"PAC File": "Berkas PAC",
"Web UI": "Antarmuka Web",
"Hotkeys": "Pintasan",
"Verge Mixed Port": "Port Campuran Verge",
"Verge Socks Port": "Port Socks Verge",
"Verge Redir Port": "Port Pengalihan Verge",
"Verge Tproxy Port": "Port Tproxy Verge",
"Verge Port": "Port Verge",
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
"WebDAV URL": "URL WebDAV",
"WebDAV Username": "Nama Pengguna WebDAV",
"WebDAV Password": "Kata Sandi WebDAV",
"Restart App": "Mulai Ulang Aplikasi",
"Restart Clash Core": "Mulai Ulang Core Clash",
"TUN Mode": "Mode TUN",
"Copy Env": "Salin Env",
"Conf Dir": "Direktori Konfigurasi",
"Core Dir": "Direktori Core",
"Logs Dir": "Direktori Log",
"Open Dir": "Buka Direktori",
"More": "Lainnya",
"Rule Mode": "Mode Aturan",
"Global Mode": "Mode Global",
"Direct Mode": "Mode Langsung",
"Proxies": "Proksi",
"Proxy Groups": "Grup Proksi",
"Proxy Provider": "Penyedia Proksi",
@@ -198,7 +165,6 @@
"Table View": "Tampilan Tabel",
"List View": "Tampilan Daftar",
"Close All": "Tutup Semua",
"Default": "Default",
"Download Speed": "Kecepatan Unduh",
"Upload Speed": "Kecepatan Unggah",
"Host": "Host",
@@ -229,6 +195,8 @@
"Settings": "Pengaturan",
"System Setting": "Pengaturan Sistem",
"Tun Mode": "Mode Tun (NIC Virtual)",
"TUN requires Service Mode": "Mode TUN memerlukan layanan",
"Install Service": "Instal Layanan",
"Reset to Default": "Setel Ulang ke Default",
"Tun Mode Info": "Mode Tun (NIC Virtual): Menangkap semua lalu lintas sistem, saat diaktifkan, tidak perlu mengaktifkan proksi sistem.",
"Stack": "Tumpukan Tun",
@@ -312,7 +280,8 @@
"Open UWP tool": "Buka alat UWP",
"Open UWP tool Info": "Sejak Windows 8, aplikasi UWP (seperti Microsoft Store) dibatasi dari mengakses layanan jaringan host lokal secara langsung, dan alat ini dapat digunakan untuk melewati pembatasan ini",
"Update GeoData": "Perbarui GeoData",
"Verge Setting": "Pengaturan Verge",
"Verge Basic Setting": "Pengaturan Dasar Verge",
"Verge Advanced Setting": "Pengaturan Lanjutan Verge",
"Language": "Bahasa",
"Theme Mode": "Mode Tema",
"theme.light": "Terang",
@@ -399,6 +368,7 @@
"Profile Reactivated": "Profil Diaktifkan Kembali",
"Only YAML Files Supported": "Hanya File YAML yang Didukung",
"Settings Applied": "Pengaturan Diterapkan",
"Installing Service...": "Memasang Layanan...",
"Service Installed Successfully": "Layanan Berhasil Diinstal",
"Service Uninstalled Successfully": "Layanan Berhasil Dicopot",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Durasi Daemon Proksi Tidak Boleh Kurang dari 1 Detik",
@@ -437,6 +407,38 @@
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
"Failed to fetch backup files": "Gagal mengambil file cadangan",
"Profile": "Profil",
"Help": "Bantuan",
"About": "Tentang",
"Theme": "Tema",
"Main Window": "Jendela Utama",
"Group Icon": "Ikon Grup",
"Menu Icon": "Ikon Menu",
"PAC File": "Berkas PAC",
"Web UI": "Antarmuka Web",
"Hotkeys": "Pintasan",
"Verge Mixed Port": "Port Campuran Verge",
"Verge Socks Port": "Port Socks Verge",
"Verge Redir Port": "Port Pengalihan Verge",
"Verge Tproxy Port": "Port Tproxy Verge",
"Verge Port": "Port Verge",
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
"WebDAV URL": "URL WebDAV",
"WebDAV Username": "Nama Pengguna WebDAV",
"WebDAV Password": "Kata Sandi WebDAV",
"Dashboard": "Dasbor",
"Restart App": "Mulai Ulang Aplikasi",
"Restart Clash Core": "Mulai Ulang Core Clash",
"TUN Mode": "Mode TUN",
"Copy Env": "Salin Env",
"Conf Dir": "Direktori Konfigurasi",
"Core Dir": "Direktori Core",
"Logs Dir": "Direktori Log",
"Open Dir": "Buka Direktori",
"More": "Lainnya",
"Rule Mode": "Mode Aturan",
"Global Mode": "Mode Global",
"Direct Mode": "Mode Langsung",
"Enable Tray Speed": "Aktifkan Tray Speed",
"LightWeight Mode": "Mode Ringan",
"LightWeight Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan",
@@ -450,10 +452,8 @@
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
"Core Changed Successfully": "Inti berhasil diubah",
"Failed to Change Core": "Gagal mengubah inti",
"Verge Basic Setting": "Pengaturan Dasar Verge",
"Verge Advanced Setting": "Pengaturan Lanjutan Verge",
"TUN requires Service Mode": "Mode TUN memerlukan layanan",
"Install Service": "Instal Layanan",
"Installing Service...": "Memasang Layanan...",
"Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem"
"Service Administrator Prompt": "Clash Verge memerlukan hak administrator untuk menginstal ulang layanan sistem",
"Default": "Default",
"Label-Test": "Tes",
"Verge Setting": "Pengaturan Verge"
}

View File

@@ -195,10 +195,11 @@
"Settings": "設定",
"System Setting": "システム設定",
"Tun Mode": "仮想ネットワークカードモード",
"TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です",
"Install Service": "サービスをインストール",
"Uninstall Service": "サービスのアンインストール",
"Reset to Default": "デフォルト値にリセット",
"Tun Mode Info": "TUN仮想ネットワークカードモードはシステムのすべてのトラフィックを制御します。有効にすると、システムプロキシを開く必要はありません。",
"TUN requires Service Mode or Admin Mode": "TUNモードはサービスモードまたは管理者モードが必要です",
"System Proxy Enabled": "システムプロキシが有効になっています。アプリケーションはプロキシを通じてネットワークにアクセスします。",
"System Proxy Disabled": "システムプロキシが無効になっています。ほとんどのユーザーはこのオプションをオンにすることをお勧めします。",
"TUN Mode Enabled": "TUNモードが有効になっています。アプリケーションは仮想ネットワークカードを通じてネットワークにアクセスします。",
@@ -254,6 +255,7 @@
"Unified Delay Info": "統一遅延を有効にすると、2回の遅延テストが行われ、接続ハンドシェイクなどによる異なるタイプのードの遅延差を解消します。",
"Log Level": "ログレベル",
"Log Level Info": "ログディレクトリのServiceフォルダ内のコアログファイルにのみ適用されます。",
"Port Config": "ポート設定",
"Random Port": "ランダムポート",
"Mixed Port": "混合プロキシポート",
"Socks Port": "SOCKSプロキシポート",
@@ -370,17 +372,16 @@
"Stopping Core...": "コアを停止中...",
"Restarting Core...": "コアを再起動中...",
"Installing Service...": "サービスをインストール中...",
"Uninstall Service": "サービスのアンインストール",
"Service Installed Successfully": "サービスのインストールに成功しました。",
"Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。",
"Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。",
"Uninstalling Service...": "サービスをアンインストール中...",
"Waiting for service to be ready...": "サービスの準備を待っています...",
"Service Installed Successfully": "サービスのインストールに成功しました。",
"Service Uninstalled Successfully": "サービスのアンインストールに成功しました。",
"Proxy Daemon Duration Cannot be Less than 1 Second": "プロキシデーモンの間隔は1秒以上に設定する必要があります。",
"Invalid Bypass Format": "無効なバイパス形式",
"Waiting for service to be ready...": "サービスの準備を待っています...",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "サービスは準備が整っていましたが、コアの再起動に問題が発生したか、サービスが利用できなくなった可能性があります。ご確認ください。",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "サービスのインストールまたはコアの再起動中に問題が発生しました。サービスが利用できない可能性があります。システムログを確認してください。",
"Service is ready and core restarted": "サービスが準備完了し、コアが再起動されました。",
"Core restarted. Service is now available.": "コアが再起動され、サービスが利用可能になりました。",
"Core Version Updated": "コアバージョンが更新されました。",
"Clash Core Restarted": "Clashコアが再起動されました。",
"GeoData Updated": "GeoDataが更新されました。",
@@ -526,7 +527,6 @@
"Unknown": "不明",
"Auto update disabled": "自動更新が無効になっています。",
"Update subscription successfully": "サブスクリプションの更新に成功しました。",
"Update failed, retrying with Clash proxy...": "サブスクリプションの更新に失敗しました。Clashプロキシを使用して再試行します...",
"Update with Clash proxy successfully": "Clashプロキシを使用して更新に成功しました。",
"Update failed even with Clash proxy": "Clashプロキシを使用しても更新に失敗しました。",
"Profile creation failed, retrying with Clash proxy...": "プロファイルの作成に失敗しました。Clashプロキシを使用して再試行します...",
@@ -558,12 +558,9 @@
"Disallowed ISP": "許可されていないインターネットサービスプロバイダー",
"Originals Only": "オリジナルのみ",
"Unsupported Country/Region": "サポートされていない国/地域",
"Configuration saved successfully": "ランダム設定を保存完了",
"Controller address copied to clipboard": "API ポートがクリップボードにコピーされました",
"Secret copied to clipboard": "API キーがクリップボードにコピーされました",
"Copy to clipboard": "クリックしてコピー",
"Port Config": "ポート設定",
"Configuration saved successfully": "ランダム設定を保存完了",
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください",
"Batch Operations": "バッチ操作",
"Delete Selected Profiles": "選択したプロファイルを削除",
"Deselect All": "すべての選択を解除",
@@ -571,5 +568,7 @@
"items": "アイテム",
"Select All": "すべて選択",
"Selected": "選択済み",
"Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました"
"Selected profiles deleted successfully": "選択したプロファイルが正常に削除されました",
"Copy to clipboard": "クリックしてコピー",
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
}

View File

@@ -127,6 +127,7 @@
"Lazy": "지연 로딩",
"Timeout": "타임아웃",
"Max Failed Times": "최대 실패 횟수",
"Interface Name": "인터페이스 이름",
"Routing Mark": "라우팅 마크",
"Include All": "모든 프록시 및 제공자 포함",
"Include All Providers": "모든 제공자 포함",
@@ -198,176 +199,52 @@
"Edit Test": "테스트 편집",
"Icon": "아이콘",
"Test URL": "테스트 URL",
"Timeout (ms)": "타임아웃 (ms)",
"Expected": "예상됨",
"URL": "URL",
"Method": "메소드",
"Failed": "실패",
"Succeed": "성공",
"Settings": "설정",
"Core Config": "코어 설정",
"Clash Setting": "Clash 설정",
"Verge Setting": "Verge 설정",
"System Setting": "시스템 설정",
"Appearance": "외관",
"Experimental Features": "실험적 기능",
"Others": "기타",
"Mixed Port": "혼합 포트",
"Allow LAN": "LAN 허용",
"IPv6": "IPv6",
"Log Level": "로그 레벨",
"Core Type": "코어 유형",
"General": "일반",
"Mode": "모드",
"Tun Mode": "Tun 모드",
"Transparent Proxy": "투명 프록시",
"Specify YAML": "YAML 지정",
"Status": "상태",
"Memory Usage": "메모리 사용량",
"Stack": "스택",
"Network": "네트워크",
"MTU": "MTU",
"Auto Route": "자동 라우팅",
"Auto Detect Interface": "인터페이스 자동 감지",
"Interface Name": "인터페이스 이름",
"Endpoint Independent Nat": "엔드포인트 독립 NAT",
"Include Reserved": "예약된 IP 포함",
"Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화",
"TCP Fast Open": "TCP 빠른 열기",
"Silent Start": "자동 시작",
"TcpConcurrent": "TCP 동시성",
"MTU": "MTU",
"Service Mode": "서비스 모드",
"System Proxy": "시스템 프록시",
"Start With System": "시스템과 함께 시작",
"Set System Proxy": "시스템 프록시 설정",
"Set as System Proxy": "시스템 프록시로 설정",
"System Proxy Status": "시스템 프록시 상태",
"Start Option": "시작 옵션",
"Start Core on Start": "시작 시 코어 시작",
"Start Core with System": "시스템과 함께 코어 시작",
"Start Core with System Proxy": "시스템 프록시와 함께 코어 시작",
"Start Core with Tun": "Tun과 함께 코어 시작",
"Silent Start Option": "자동 시작 옵션",
"Hidden Window on Start": "시작 시 창 숨기기",
"Log Notice": "로그 알림",
"Warning": "경고",
"Error": "오류",
"Silent Start": "자동 시작",
"Clash Setting": "Clash 설정",
"IPv6": "IPv6",
"Log Level": "로그 레벨",
"Mixed Port": "혼합 포트",
"Verge Basic Setting": "Verge 기본 설정",
"Language": "언어",
"Theme Mode": "테마 모드",
"Tray Click Event": "트레이 클릭 이벤트",
"Show Main Window": "메인 창 표시",
"Show Tray Menu": "트레이 메뉴 표시",
"Open Config Folder": "설정 폴더 열기",
"Open Dashboard": "대시보드 열기",
"Hotkey Setting": "단축키 설정",
"Misc Setting": "기타 설정",
"Layout Setting": "레이아웃 설정",
"Update Setting": "업데이트 설정",
"Enable Hotkeys": "단축키 활성화",
"System Hotkey": "시스템 단축키",
"Hotkey Enable": "단축키 활성화",
"Require Clash Core Running": "Clash 코어 실행 필요",
"Copy Env Type": "환경 유형 복사",
"Copy Success": "복사 성공",
"Start Page": "시작 페이지",
"Startup Script": "시작 스크립트",
"Icon Group Type": "아이콘 그룹 유형",
"Always": "항상",
"On Update": "업데이트 시",
"By Traffic": "트래픽별",
"Web UI List": "웹 UI 목록",
"Installed Web UI": "설치된 웹 UI",
"Built-in Web UI": "내장 웹 UI",
"Current Config": "현재 설정",
"System Config": "시스템 설정",
"Port": "포트",
"WebUI Current Port": "웹 UI 현재 포트",
"Theme": "테마",
"Light": "라이트",
"Dark": "다크",
"Auto": "자동",
"System": "시스템",
"Proxy Item Width": "프록시 항목 너비",
"Proxy Item Height": "프록시 항목 높이",
"Compact Mode": "압축 모드",
"Git Proxy": "Git 프록시",
"Enable API": "API 활성화",
"Enable Lan": "LAN 활성화",
"Select a config file": "설정 파일 선택",
"Open Config Dir": "설정 디렉토리 열기",
"System Proxy Permission": "시스템 프록시 권한",
"System Stack Type": "시스템 스택 유형",
"Undefined stack": "정의되지 않은 스택",
"Auto Start": "자동 시작",
"Mixin": "혼합",
"Set as System Auto Proxy": "시스템 자동 프록시로 설정",
"System Auto Proxy Status": "시스템 자동 프록시 상태",
"Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)",
"Primary Color": "기본 색상",
"Layout Setting": "레이아웃 설정",
"Traffic Graph": "트래픽 그래프",
"Memory Usage": "메모리 사용량",
"Auto Delay Detection": "자동 지연 감지",
"Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다",
"Hotkey Setting": "단축키 설정",
"Filter": "필터",
"Import Subscription Successful": "구독 가져오기 성공",
"Username": "사용자 이름",
"Password": "비밀번호",
"Auth Proxy": "인증 프록시",
"Geox User": "Geox 사용자",
"Geox Password": "Geox 비밀번호",
"Log File": "로그 파일",
"Enable Clash.Meta Logs": "Clash.Meta 로그 활성화",
"Verge Log": "Verge 로그",
"Enable Verge Logs": "Verge 로그 활성화",
"Traffic Graph": "트래픽 그래프",
"Profile Token": "프로필 토큰",
"Profile User Agent": "프로필 사용자 에이전트",
"The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.",
"Profile Format": "프로필 포맷",
"The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.",
"Theme Color": "테마 색상",
"Primary Color": "기본 색상",
"Customize primary color": "기본 색상 사용자 정의",
"Danger Zone": "위험 영역",
"Reset Verge Theme": "Verge 테마 재설정",
"Inject CSS": "CSS 주입",
"Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입",
"Inject HTML": "HTML 주입",
"Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)",
"Capture": "캡처",
"Color Scheme": "색상 구성표",
"Default": "기본값",
"Pink": "분홍색",
"Red": "빨간색",
"Yellow": "노란색",
"Green": "녹색",
"Cyan": "청록색",
"Blue": "파란색",
"Purple": "보라색",
"Proxy Detail": "프록시 상세",
"Address": "주소",
"Filter": "필터",
"Check Updates on Start": "시작 시 업데이트 확인",
"For Alpha Version": "알파 버전용",
"Latest Build Version": "최신 빌드 버전",
"Check Updates": "업데이트 확인",
"Proxy Setting": "프록시 설정",
"WebDav Setting": "WebDav 설정",
"WebDav Upload": "WebDav 업로드",
"WebDav Download": "WebDav 다운로드",
"Clean Cache": "캐시 정리",
"Check Network": "네트워크 확인",
"WebDav Status": "WebDav 상태",
"WebDav URL": "WebDav URL",
"WebDav Username": "WebDav 사용자 이름",
"WebDav Password": "WebDav 비밀번호",
"Update Interval(minute)": "업데이트 간격(분)",
"Skip Cert Verify": "인증서 확인 건너뛰기",
"Import Subscription Successful": "구독 가져오기 성공",
"Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공",
"Update failed, retrying with Clash proxy...": "업데이트 실패, Clash 프록시로 재시도 중...",
"Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패",
"Theme": "테마",
"Config Validation Failed": "설정 검증 실패",
"Boot Config Validation Failed": "부팅 설정 검증 실패",
"Core Change Config Validation Failed": "코어 변경 설정 검증 실패",
"Config Validation Failed": "설정 검증 실패",
"Config Validation Process Terminated": "설정 검증 프로세스 종료됨",
"Script File Error": "스크립트 파일 오류",
"Script Syntax Error": "스크립트 구문 오류",
"Script Missing Main": "스크립트 메인 없음",
"File Not Found": "파일을 찾을 수 없음",
"Script File Error": "스크립트 파일 오류",
"Core Changed Successfully": "코어 변경 성공",
"Failed to Change Core": "코어 변경 실패",
"YAML Syntax Error": "YAML 구문 오류",
"YAML Read Error": "YAML 읽기 오류",
"YAML Mapping Error": "YAML 매핑 오류",
@@ -377,28 +254,150 @@
"Merge File Mapping Error": "병합 파일 매핑 오류",
"Merge File Key Error": "병합 파일 키 오류",
"Merge File Error": "병합 파일 오류",
"Core Changed Successfully": "코어 변경 성공",
"Failed to Change Core": "코어 변경 실패",
"Copy Success": "복사 성공",
"Copy Failed": "복사 실패",
"Update with Clash proxy successfully": "Clash 프록시로 업데이트 성공",
"Update failed even with Clash proxy": "Clash 프록시로도 업데이트 실패",
"Failed": "실패",
"Address": "주소",
"Allow LAN": "LAN 허용",
"Always": "항상",
"Appearance": "외관",
"Auth Proxy": "인증 프록시",
"Authorization for requests coming through HTTP Proxy (e.g. local connections)": "HTTP 프록시를 통한 요청에 대한 인증 (예: 로컬 연결)",
"Auto": "자동",
"Auto Start": "자동 시작",
"Blue": "파란색",
"Built-in Web UI": "내장 웹 UI",
"By Traffic": "트래픽별",
"Cannot Import Empty Subscription URL": "빈 구독 URL을 가져올 수 없습니다",
"Profile Already Exists": "프로필이 이미 존재합니다",
"Input Subscription URL": "구독 URL 입력",
"Create Profile Successful": "프로필 생성 성공",
"Capture": "캡처",
"Check Network": "네트워크 확인",
"Check Updates": "업데이트 확인",
"Check Updates on Start": "시작 시 업데이트 확인",
"Clean Cache": "캐시 정리",
"Color Scheme": "색상 구성표",
"Compact Mode": "압축 모드",
"Copy Failed": "복사 실패",
"Core Config": "코어 설정",
"Core Type": "코어 유형",
"Create Profile Failed": "프로필 생성 실패",
"Patch Profile Successful": "프로필 패치 성공",
"Patch Profile Failed": "프로필 패치 실패",
"Delete Profile Successful": "프로필 삭제 성공",
"Create Profile Successful": "프로필 생성 성공",
"Current Config": "현재 설정",
"Customize primary color": "기본 색상 사용자 정의",
"Cyan": "청록색",
"Danger Zone": "위험 영역",
"Dark": "다크",
"Default": "기본값",
"Delete Profile Failed": "프로필 삭제 실패",
"Select Active Profile Successful": "활성 프로필 선택 성공",
"Delete Profile Successful": "프로필 삭제 성공",
"Enable API": "API 활성화",
"Enable Clash.Meta Logs": "Clash.Meta 로그 활성화",
"Enable Default DNS Hijack": "기본 DNS 하이재킹 활성화",
"Enable Hotkeys": "단축키 활성화",
"Enable Lan": "LAN 활성화",
"Enable Verge Logs": "Verge 로그 활성화",
"Endpoint Independent Nat": "엔드포인트 독립 NAT",
"Error": "오류",
"Expected": "예상됨",
"Experimental Features": "실험적 기능",
"For Alpha Version": "알파 버전용",
"General": "일반",
"Geox Password": "Geox 비밀번호",
"Geox User": "Geox 사용자",
"Git Proxy": "Git 프록시",
"Green": "녹색",
"Hidden Window on Start": "시작 시 창 숨기기",
"Hotkey Enable": "단축키 활성화",
"Icon Group Type": "아이콘 그룹 유형",
"Include Reserved": "예약된 IP 포함",
"Inject CSS": "CSS 주입",
"Inject HTML": "HTML 주입",
"Inject a custom CSS content into the GUI": "사용자 정의 CSS 내용을 GUI에 주입",
"Inject a custom HTML content into the GUI (appended in body)": "사용자 정의 HTML 내용을 GUI에 주입 (본문에 추가)",
"Input Subscription URL": "구독 URL 입력",
"Installed Web UI": "설치된 웹 UI",
"Latest Build Version": "최신 빌드 버전",
"Light": "라이트",
"Log File": "로그 파일",
"Log Notice": "로그 알림",
"Method": "메소드",
"Misc Setting": "기타 설정",
"Mixin": "혼합",
"Mode": "모드",
"Network": "네트워크",
"On Update": "업데이트 시",
"Open Config Dir": "설정 디렉토리 열기",
"Open Config Folder": "설정 폴더 열기",
"Open Dashboard": "대시보드 열기",
"Others": "기타",
"Patch Profile Failed": "프로필 패치 실패",
"Patch Profile Successful": "프로필 패치 성공",
"Pink": "분홍색",
"Port": "포트",
"Profile Already Exists": "프로필이 이미 존재합니다",
"Profile Format": "프로필 포맷",
"Profile Token": "프로필 토큰",
"Profile User Agent": "프로필 사용자 에이전트",
"Proxy Detail": "프록시 상세",
"Proxy Item Height": "프록시 항목 높이",
"Proxy Item Width": "프록시 항목 너비",
"Proxy Setting": "프록시 설정",
"Purple": "보라색",
"Red": "빨간색",
"Require Clash Core Running": "Clash 코어 실행 필요",
"Reset Verge Theme": "Verge 테마 재설정",
"Select Active Profile Failed": "활성 프로필 선택 실패",
"View Profile-Runtime": "프로필-런타임 보기",
"View Profile-Content": "프로필-내용 보기",
"View Profile-Original": "프로필-원본 보기",
"View Profile-Script": "프로필-스크립트 보기",
"View Profile-Merge": "프로필-병합 보기",
"Update Successful": "업데이트 성공",
"Select Active Profile Successful": "활성 프로필 선택 성공",
"Select a config file": "설정 파일 선택",
"Set System Proxy": "시스템 프록시 설정",
"Set as System Auto Proxy": "시스템 자동 프록시로 설정",
"Set as System Proxy": "시스템 프록시로 설정",
"Silent Start Option": "자동 시작 옵션",
"Skip Cert Verify": "인증서 확인 건너뛰기",
"Specify YAML": "YAML 지정",
"Start Core on Start": "시작 시 코어 시작",
"Start Core with System": "시스템과 함께 코어 시작",
"Start Core with System Proxy": "시스템 프록시와 함께 코어 시작",
"Start Core with Tun": "Tun과 함께 코어 시작",
"Start Option": "시작 옵션",
"Start With System": "시스템과 함께 시작",
"Status": "상태",
"Succeed": "성공",
"System": "시스템",
"System Auto Proxy Status": "시스템 자동 프록시 상태",
"System Config": "시스템 설정",
"System Hotkey": "시스템 단축키",
"System Proxy Permission": "시스템 프록시 권한",
"System Proxy Status": "시스템 프록시 상태",
"System Stack Type": "시스템 스택 유형",
"TCP Fast Open": "TCP 빠른 열기",
"TcpConcurrent": "TCP 동시성",
"The User Agent to use when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 사용할 사용자 에이전트입니다.",
"The expected content type to send in the `Accept` header when refreshing a subscription profile.": "구독 프로필을 새로 고칠 때 `Accept` 헤더에 보낼 예상 컨텐츠 타입입니다.",
"Theme Color": "테마 색상",
"Timeout (ms)": "타임아웃 (ms)",
"Transparent Proxy": "투명 프록시",
"URL": "URL",
"Undefined stack": "정의되지 않은 스택",
"Update Failed": "업데이트 실패",
"Auto Delay Detection": "자동 지연 감지",
"Auto Delay Detection Info": "백그라운드에서 현재 노드의 지연을 주기적으로 검사합니다"
"Update Interval(minute)": "업데이트 간격(분)",
"Update Setting": "업데이트 설정",
"Update Successful": "업데이트 성공",
"Verge Log": "Verge 로그",
"Verge Setting": "Verge 설정",
"View Profile-Content": "프로필-내용 보기",
"View Profile-Merge": "프로필-병합 보기",
"View Profile-Original": "프로필-원본 보기",
"View Profile-Runtime": "프로필-런타임 보기",
"View Profile-Script": "프로필-스크립트 보기",
"Warning": "경고",
"Web UI List": "웹 UI 목록",
"WebDav Download": "WebDav 다운로드",
"WebDav Password": "WebDav 비밀번호",
"WebDav Setting": "WebDav 설정",
"WebDav Status": "WebDav 상태",
"WebDav URL": "WebDav URL",
"WebDav Upload": "WebDav 업로드",
"WebDav Username": "WebDav 사용자 이름",
"WebUI Current Port": "웹 UI 현재 포트",
"Yellow": "노란색"
}

View File

@@ -508,7 +508,6 @@
"Fake IP Filter Mode": "Sahte IP Filtre Modu",
"Enable IPv6 DNS resolution": "IPv6 DNS çözümlemesini etkinleştir",
"Prefer H3": "H3'ü Tercih Et",
"DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır",
"Respect Rules": "Kurallara Uy",
"DNS connections follow routing rules": "DNS bağlantıları yönlendirme kurallarını takip eder",
"Use Hosts": "Hosts Kullan",
@@ -575,7 +574,6 @@
"Unknown": "Bilinmiyor",
"Auto update disabled": "Otomatik güncelleme devre dışı",
"Update subscription successfully": "Abonelik başarıyla güncellendi",
"Update failed, retrying with Clash proxy...": "Güncelleme başarısız oldu, Clash vekil ile yeniden deneniyor...",
"Update with Clash proxy successfully": "Clash vekil ile güncelleme başarılı",
"Update failed even with Clash proxy": "Clash vekil ile bile güncelleme başarısız oldu",
"Profile creation failed, retrying with Clash proxy...": "Profil oluşturma başarısız oldu, Clash vekil ile yeniden deneniyor...",
@@ -616,5 +614,6 @@
"items": "öğeler",
"Select All": "Tümünü Seç",
"Selected": "Seçildi",
"Selected profiles deleted successfully": "Seçili profiller başarıyla silindi"
"Selected profiles deleted successfully": "Seçili profiller başarıyla silindi",
"DNS DOH uses HTTP/3": "DNS DOH HTTP/3 kullanır"
}

View File

@@ -21,7 +21,6 @@
"Label-Connections": "Тоташулар",
"Label-Rules": "Кагыйдәләр",
"Label-Logs": "Логлар",
"Label-Test": "Тест",
"Label-Settings": "Көйләүләр",
"Proxies": "Прокси",
"Proxy Groups": "Прокси төркемнәре",
@@ -166,7 +165,6 @@
"Table View": "Таблица күзаллау",
"List View": "Исемлек күзаллау",
"Close All": "Барысын да ябу",
"Default": "Башлангыч",
"Download Speed": "Йөкләү тизлеге",
"Upload Speed": "Йөкләү (чыгару) тизлеге",
"Host": "Хост",
@@ -197,6 +195,8 @@
"Settings": "Көйләүләр",
"System Setting": "Система көйләүләре",
"Tun Mode": "Tun режимы (виртуаль челтәр адаптеры)",
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
"Install Service": "Хезмәтне урнаштыру",
"Reset to Default": "Башлангычка кайтару",
"Tun Mode Info": "Tun режимы бөтен системаның трафигын тотып ала. Аны кабызган очракта системалы проксины аерым кабызу таләп ителми.",
"Stack": "Стек",
@@ -280,7 +280,8 @@
"Open UWP tool": "UWP инструментын ачу",
"Open UWP tool Info": "Windows 8'дән башлап UWP кушымталары (Microsoft Store кебек) локаль хосттагы челтәр хезмәтләренә турыдан-туры тоташа алмый. Бу инструмент әлеге чикләүне әйләнеп узарга ярдәм итә",
"Update GeoData": "GeoData яңарту",
"Verge Setting": "Verge көйләүләре",
"Verge Basic Setting": "Verge Төп көйләүләр",
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр",
"Language": "Тел",
"Theme Mode": "Теманың режимы",
"theme.light": "Якты",
@@ -296,8 +297,6 @@
"Theme Setting": "Тема көйләүләре",
"Primary Color": "Төп төс",
"Secondary Color": "Икенче төс",
"Primary Text Color": "Төп текст төсе",
"Secondary Text Color": "Икенче текст төсе",
"Info Color": "Мәгълүмат төсе",
"Warning Color": "Кисәтү төсе",
"Error Color": "Хата төсе",
@@ -367,6 +366,7 @@
"Profile Reactivated": "Профиль яңадан активлаштырылды",
"Only YAML Files Supported": "Фәкать YAML-файллар гына хуплана",
"Settings Applied": "Көйләүләр кулланылды",
"Installing Service...": "Хезмәт урнаштырыла...",
"Service Installed Successfully": "Сервис уңышлы урнаштырылды",
"Service Uninstalled Successfully": "Сервис уңышлы салдырылды",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Прокси-демон эш вакыты 1 секундтан ким була алмый",
@@ -380,7 +380,6 @@
"Clash Core Restarted": "Clash ядросы яңадан башланды",
"GeoData Updated": "GeoData яңартылды",
"Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган",
"Import subscription successful": "Подписка уңышлы импортланды",
"WebDAV Server URL": "WebDAV сервер URL-ы (http(s)://)",
"Username": "Кулланучы исеме",
"Password": "Пароль",
@@ -450,10 +449,11 @@
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
"Core Changed Successfully": "Ядро уңышлы алыштырылды",
"Failed to Change Core": "Ядро алыштыру уңышсыз булды",
"Verge Basic Setting": "Verge Төп көйләүләр",
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр",
"TUN requires Service Mode": "TUN режимы хезмәт күрсәтүне таләп итә",
"Install Service": "Хезмәтне урнаштыру",
"Installing Service...": "Хезмәт урнаштырыла...",
"Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә"
"Service Administrator Prompt": "Clash Verge система хезмәтен яңадан урнаштыру өчен администратор хокукларын таләп итә",
"Default": "Башлангыч",
"Import subscription successful": "Подписка уңышлы импортланды",
"Label-Test": "Тест",
"Primary Text Color": "Төп текст төсе",
"Secondary Text Color": "Икенче текст төсе",
"Verge Setting": "Verge көйләүләре"
}

View File

@@ -305,7 +305,7 @@
"Socks Port": "SOCKS 代理端口",
"Http Port": "HTTP(S) 代理端口",
"Redir Port": "Redir 透明代理端口",
"TPROXY Port": "TPROXY 透明代理端口",
"Tproxy Port": "Tproxy 透明代理端口",
"Port settings saved": "端口设置已保存",
"Failed to save port settings": "端口设置保存失败",
"External": "外部控制",
@@ -426,6 +426,8 @@
"Uninstalling Service...": "卸载服务中...",
"Service Installed Successfully": "已成功安装服务",
"Service Uninstalled Successfully": "已成功卸载服务",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
"Invalid Bypass Format": "无效的代理绕过格式",
"Waiting for service to be ready...": "等待服务准备就绪...",
"Service not ready, retrying attempt {count}/{total}...": "服务未就绪,正在重试 {{count}}/{{total}} 次...",
"Failed to check service status, retrying attempt {count}/{total}...": "检查服务状态失败,正在重试 {{count}}/{{total}} 次...",
@@ -436,8 +438,6 @@
"Fallback core restart also failed: {message}": "后备内核重启也失败了: {{message}}",
"Service is ready and core restarted": "服务已就绪,内核已重启",
"Core restarted. Service is now available.": "内核已重启,服务现已可用",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
"Invalid Bypass Format": "无效的代理绕过格式",
"Clash Port Modified": "Clash 端口已修改",
"Port Conflict": "端口冲突",
"Restart Application to Apply Modifications": "重启 Verge 以应用修改",
@@ -628,7 +628,6 @@
"Unknown": "未知",
"Auto update disabled": "自动更新已禁用",
"Update subscription successfully": "订阅更新成功",
"Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新",
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
"Update failed even with Clash proxy": "使用 Clash 代理更新也失败",
"Profile creation failed, retrying with Clash proxy...": "订阅创建失败,尝试使用 Clash 代理创建",
@@ -713,5 +712,8 @@
"Allow Auto Update": "允许自动更新",
"Menu reorder mode": "菜单排序模式",
"Unlock menu order": "解锁菜单排序",
"Lock menu order": "锁定菜单排序"
"Lock menu order": "锁定菜单排序",
"Open App Log": "应用日志",
"Open Core Log": "内核日志",
"TPROXY Port": "TPROXY 透明代理端口"
}

View File

@@ -24,6 +24,7 @@
"Label-Logs": "日 誌",
"Label-Unlock": "解 鎖",
"Label-Settings": "設 定",
"Proxies": "代理",
"Proxy Groups": "代理組",
"Proxy Chain Mode": "鏈式代理模式",
"Connect": "連線",
@@ -37,8 +38,16 @@
"rule": "規則",
"global": "全域",
"direct": "直連",
"script": "指令碼",
"Chain Proxy": "🔗 鏈式代理",
"Chain Proxy Config": "鏈式代理設定",
"Proxy Rules": "代理規則",
"Select Rules": "选择规则",
"Click nodes in order to add to proxy chain": "依序點擊節點新增到鏈式代理中",
"No proxy chain configured": "暫無鏈式代理設定",
"Proxy Order": "代理順序",
"timeout": "逾時",
"Clear All": "全部清除",
"script": "指令碼",
"locate": "目前節點",
"Delay check": "延遲測試",
"Sort by default": "預設排序",
@@ -144,6 +153,8 @@
"Group Name Already Exists": "代理組名稱已存在",
"Extend Config": "擴充覆寫設定",
"Extend Script": "擴充指令碼",
"Global Merge": "全域擴充覆寫設定",
"Global Script": "全域擴充指令碼",
"Type": "類型",
"Name": "名稱",
"Descriptions": "描述",
@@ -152,6 +163,7 @@
"Choose File": "選擇檔案",
"Use System Proxy": "使用系統代理更新",
"Use Clash Proxy": "使用內核代理更新",
"Accept Invalid Certs (Danger)": "允許無效憑證(危險)",
"Refresh": "重整",
"Home": "首頁",
"Select": "使用",
@@ -159,13 +171,18 @@
"Edit File": "編輯檔案",
"Open File": "開啟檔案",
"Update": "更新",
"Update via proxy": "更新(代理)",
"Update(Proxy)": "更新(代理)",
"Confirm deletion": "確認刪除",
"This operation is not reversible": "此操作無法復原",
"Script Console": "指令碼控制台輸出",
"To Top": "前往頂端",
"To End": "前往末端",
"Connections": "連線",
"Table View": "表格檢視",
"List View": "列表檢視",
"Close All": "關閉全部",
"Close All Connections": "關閉全部連線",
"Upload": "上傳",
"Download": "下載",
"Download Speed": "下載速度",
@@ -199,29 +216,46 @@
"Test URL": "測試網址",
"Settings": "設定",
"System Setting": "系統設定",
"Tun Mode": "虛擬網卡模式",
"TUN requires Service Mode or Admin Mode": "虛擬網卡模式需要服務模式或管理員模式",
"Tun Mode": "虛擬網路介面卡模式",
"Install Service": "安裝服務",
"Install Service failed": "安裝服務失敗",
"Uninstall Service": "解除安裝服務",
"Restart Core failed": "重新啟動內核失敗",
"Reset to Default": "重設為預設值",
"Tun Mode Info": "TUN虛擬網路介面卡模式接管系統所有流量啟用時無需開啟系統代理",
"TUN requires Service Mode or Admin Mode": "虛擬網路介面卡模式需要安裝服務模式或以系統管理員身分執行",
"TUN Mode automatically disabled due to service unavailable": "由於服務不可使用,虛擬網路介面卡模式已自動停用",
"Failed to disable TUN Mode automatically": "自動停用虛擬網路介面卡模式失敗",
"System Proxy Enabled": "系統代理已啟用,您的應用程式將透過代理存取網路",
"System Proxy Disabled": "系統代理已關閉,建議大多數使用者開啟此選項",
"TUN Mode Enabled": "虛擬網卡模式已啟用,應用程式將透過虛擬網路介面卡存取網路",
"TUN Mode Disabled": "虛擬網卡模式已關閉,適用於特殊應用程式",
"TUN Mode Service Required": "虛擬網卡模式需要服務模式,請先安裝服務",
"TUN Mode Intercept Info": "虛擬網卡模式可以接管所有應用程式流量,適用於不遵循系統代理設定的特殊應用程式",
"Stack": "虛擬網卡模式堆疊",
"Device": "TUN 網路介面卡名稱",
"TUN Mode Enabled": "虛擬網路介面卡模式已啟用,應用程式將透過虛擬網路介面卡存取網路",
"TUN Mode Disabled": "虛擬網路介面卡模式已關閉,適用於特殊應用程式",
"TUN Mode Service Required": "虛擬網路介面卡模式需要服務模式,請先安裝服務",
"TUN Mode Intercept Info": "虛擬網路介面卡模式可以接管所有應用程式流量,適用於不遵循系統代理設定的特殊應用程式",
"Core communication error": "內核通信錯誤",
"Rule Mode Description": "基於預設規則智慧判斷流量走向,提供更靈活的代理策略",
"Global Mode Description": "所有流量均透過代理伺服器,適用於需要全域科學上網的場合",
"Direct Mode Description": "所有流量不透過代理伺服器但經過Clash內核轉發連線目標伺服器適用於需要透過內核進行分流的特定場合",
"Stack": "虛擬網路介面卡模式堆疊",
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服務模式下使用",
"Device": "虛擬網路介面卡名稱",
"Auto Route": "自動設定全域路由",
"Strict Route": "嚴格路由",
"Auto Detect Interface": "自動偵測流量輸出介面",
"DNS Hijack": "DNS 綁架",
"MTU": "最大傳輸單位",
"Service Mode": "服務模式",
"Service Mode Info": "啟用虛擬網路介面卡模式前請先安裝服務模式該服務啟動的內核處理程序可以獲得安裝虛擬網路介面卡TUN 模式)的權限",
"Current State": "当前状态",
"pending": "等待中",
"installed": "已安装",
"uninstall": "未安装",
"active": "作用中",
"unknown": "未知",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "提示資訊:請確認 Clash Verge Service 已安裝並啟用",
"Install": "安裝",
"Uninstall": "解除安裝",
"Disable Service Mode": "停用服務模式",
"System Proxy": "系統代理",
"System Proxy Info": "修改作業系統的代理設定,如果開啟失敗,可手動修改作業系統的代理設定",
"System Proxy Setting": "系統代理設定",
@@ -237,6 +271,7 @@
"Proxy Guard Info": "開啟以防止其他軟體修改作業系統的代理設定",
"Guard Duration": "代理守護間隔",
"Always use Default Bypass": "始終使用預設繞過",
"Use Bypass Check": "啟用代理繞過檢查",
"Proxy Bypass": "代理繞過設定:",
"Bypass": "目前繞過:",
"Use PAC Mode": "使用 PAC 模式",
@@ -246,6 +281,10 @@
"Administrator mode may not support auto launch": "管理員模式可能不支援開機自啟",
"Silent Start": "靜默啟動",
"Silent Start Info": "程序啟動時以後台模式執行,不顯示程序面板",
"Hover Jump Navigator": "懸浮跳轉導航",
"Hover Jump Navigator Info": "滑鼠懸停在字母上時自動捲動到對應代理組",
"Hover Jump Navigator Delay": "懸浮跳轉導航延遲",
"Hover Jump Navigator Delay Info": "滑鼠懸停後觸發自動跳轉前等待的毫秒數",
"TG Channel": "Telegram 頻道",
"Manual": "使用手冊",
"Github Repo": "GitHub 專案位址",
@@ -259,13 +298,16 @@
"Unified Delay Info": "開啟統一延遲時,會進行兩次延遲測試,以消除連線握手等帶來的不同類型節點的延遲差異",
"Log Level": "日誌等級",
"Log Level Info": "僅對日誌目錄 Service 資料夾下的內核日誌檔案生效",
"Port Config": "連接埠設定",
"Random Port": "隨機連接埠",
"Mixed Port": "混合代理連接埠",
"Socks Port": "SOCKS 代理連接埠",
"Http Port": "HTTP(S) 代理連接埠",
"Redir Port": "Redir 透明代理連接埠",
"Tproxy Port": "TPROXY 透明代理連接埠",
"Port settings saved": "連結埠設定已儲存",
"Failed to save port settings": "連結埠設定儲存失敗",
"External": "外部控制",
"Enable External Controller": "啟用外部控制器",
"External Controller": "外部控制器監聽位址",
"Core Secret": "API 存取金鑰",
"Recommended": "建議設定",
@@ -277,7 +319,9 @@
"Restart": "重啟內核",
"Release Version": "正式版",
"Alpha Version": "預覽版",
"Please Enable Service Mode": "請先安裝並啟用服務模式",
"Please enter your root password": "請輸入您的 root 密碼",
"Grant": "授權",
"Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8 開始限制 UWP 應用程式如Microsoft Store直接存取本機主機的網路服務使用此工具可繞過該限制",
"Update GeoData": "更新 GeoData",
@@ -312,17 +356,13 @@
"Memory Usage": "內核佔用",
"Memory Cleanup": "點擊清理記憶體",
"Proxy Group Icon": "代理組圖示",
"Hover Jump Navigator": "懸浮跳轉導航",
"Hover Jump Navigator Info": "滑鼠懸停在字母上時自動捲動到對應代理組",
"Hover Jump Navigator Delay": "懸浮跳轉導航延遲",
"Hover Jump Navigator Delay Info": "滑鼠懸停後觸發自動跳轉前等待的毫秒數",
"Nav Icon": "導覽列圖示",
"Monochrome": "單色圖示",
"Colorful": "彩色圖示",
"Tray Icon": "系統匣圖示",
"Common Tray Icon": "一般系統匣圖示",
"System Proxy Tray Icon": "系統代理系統匣圖示",
"Tun Tray Icon": "虛擬網卡模式系統匣圖示",
"Tun Tray Icon": "虛擬網路介面卡模式系統匣圖示",
"Miscellaneous": "雜項設定",
"App Log Level": "應用程式日誌等級",
"Auto Close Connections": "自動關閉連線",
@@ -347,7 +387,7 @@
"clash_mode_global": "全域模式",
"clash_mode_direct": "直連模式",
"toggle_system_proxy": "開啟/關閉系統代理",
"toggle_tun_mode": "開啟/關閉 虛擬網卡模式",
"toggle_tun_mode": "開啟/關閉 虛擬網路介面卡模式",
"entry_lightweight_mode": "進入輕量模式",
"Backup Setting": "備份設定",
"Backup Setting Info": "支援本機或 WebDAV 方式備份配置檔案",
@@ -362,6 +402,7 @@
"Break Change Update Error": "此版本為重大更新,不支援應用程式內更新,請解除安裝後手動下載安裝",
"Open Dev Tools": "開發人員工具",
"Export Diagnostic Info": "匯出診斷資訊",
"Export Diagnostic Info For Issue Reporting": "匯出診斷資訊用於問題回報",
"Exit": "離開",
"Verge Version": "Verge 版本",
"ReadOnly": "唯讀",
@@ -374,26 +415,37 @@
"Profile Imported Successfully": "匯入設定檔成功",
"Profile Switched": "訂閱已切換",
"Profile Reactivated": "訂閱已啟用",
"Profile switch interrupted by new selection": "配置切換被新的選擇中斷",
"Only YAML Files Supported": "僅支援 YAML 檔案",
"Settings Applied": "設定已套用",
"Stopping Core...": "內核停止中...",
"Restarting Core...": "內核重啟中...",
"Installing Service...": "安裝服務中...",
"Uninstall Service": "解除安裝服務",
"Service Installed Successfully": "已成功安裝服務",
"Service is ready and core restarted": "服務已就緒,內核已重啟",
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "服務已就緒,但內核重啟可能存在問題或服務已不可用。請檢查。",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "服務安裝或內核重啟遇到問題。服務可能不可用。請檢查系統日誌。",
"Uninstalling Service...": "服務解除安裝中...",
"Waiting for service to be ready...": "等待服務就緒...",
"Service Installed Successfully": "已成功安裝服務",
"Service Uninstalled Successfully": "已成功解除安裝服務",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守護間隔時間不得低於 1 秒",
"Invalid Bypass Format": "無效的代理繞過格式",
"Waiting for service to be ready...": "等待服務就緒...",
"Service not ready, retrying attempt {count}/{total}...": "服務未就緒,正在重試 {{count}}/{{total}} 次...",
"Failed to check service status, retrying attempt {count}/{total}...": "檢查服務狀態失敗,正在重試 {{count}}/{{total}} 次...",
"Service did not become ready after attempts. Proceeding with core restart.": "服務在嘗試後仍未就緒,正在重新啟動內核。",
"Service was ready, but core restart might have issues or service became unavailable. Please check.": "服務已就緒,但內核重啟可能存在問題或服務已不可用。請檢查。",
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.": "服務安裝或內核重啟遇到問題。服務可能不可用。請檢查系統日誌。",
"Attempting to restart core as a fallback...": "嘗試重新啟動內核作為備援方案...",
"Fallback core restart also failed: {message}": "被園內核重新啟動也失敗了:{{message}}",
"Service is ready and core restarted": "服務已就緒,內核已重啟",
"Core restarted. Service is now available.": "內核已重啟,服務已就緒",
"Clash Port Modified": "Clash 連結埠已修改",
"Port Conflict": "連結埠衝突",
"Restart Application to Apply Modifications": "重新啟動 Verge 以套用修改",
"External Controller Address Modified": "外部控制器監聽位址已修改",
"Permissions Granted Successfully for _clash Core": "{{core}} 內核授權成功",
"Core Version Updated": "內核版本已更新",
"Clash Core Restarted": "已重啟 Clash 內核",
"GeoData Updated": "已更新 GeoData",
"Currently on the Latest Version": "目前已是最新版本",
"Already Using Latest Core": "已是最新內核版本",
"Import Subscription Successful": "匯入訂閱成功",
"WebDAV Server URL": "WebDAV 伺服器位址 http(s)://",
"Username": "使用者名稱",
@@ -411,6 +463,7 @@
"Invalid WebDAV URL": "無效的 WebDAV 伺服器位址格式",
"Username Required": "使用者名稱不能為空",
"Password Required": "密碼不能為空",
"Failed to Fetch Backups": "取得備份檔案失敗",
"WebDAV Config Saved": "WebDAV 配置儲存成功",
"WebDAV Config Save Failed": "儲存 WebDAV 配置失敗: {{error}}",
"Backup Created": "備份建立成功",
@@ -424,14 +477,33 @@
"Export Backup": "匯出備份",
"Restore Backup": "還原備份",
"Backup Time": "備份時間",
"Confirm to delete this backup file?": "確認是否刪除此備份檔案嗎?",
"Confirm to restore this backup file?": "確認還原此份檔案嗎?",
"Restore Success, App will restart in 1s": "還原成功,應用程式將在 1 秒後重啟",
"Failed to fetch backup files": "取得備份檔案失敗",
"Profile": "配置檔案",
"Profile": "配置",
"Help": "幫助",
"About": "關於",
"Theme": "主題",
"Main Window": "主視窗",
"Group Icon": "群組圖示",
"Menu Icon": "選單圖示",
"PAC File": "PAC 檔案",
"Web UI": "網頁介面",
"Hotkeys": "快捷键",
"Verge Mixed Port": "Verge 混合連結埠",
"Verge Socks Port": "Verge SOCKS 連結埠",
"Verge Redir Port": "Verge 重新導向連結埠",
"Verge Tproxy Port": "Verge 透明代理連結埠",
"Verge Port": "Verge 連結埠",
"Verge HTTP Enabled": "Verge HTTP 已啟用",
"WebDAV URL": "WebDAV 位址",
"WebDAV Username": "WebDAV 用戶名",
"WebDAV Password": "WebDAV 密碼",
"Dashboard": "儀表板",
"Restart App": "重啟應用程式",
"Restart Clash Core": "重啟 Clash 內核",
"TUN Mode": "虛擬網卡模式",
"TUN Mode": "虛擬網路介面卡模式",
"Copy Env": "複製環境變數",
"Conf Dir": "配置目錄",
"Core Dir": "內核目錄",
@@ -471,6 +543,10 @@
"Merge File Mapping Error": "覆寫檔案映射錯誤,變更已撤銷",
"Merge File Key Error": "覆寫檔案鍵錯誤,變更已撤銷",
"Merge File Error": "覆寫檔案錯誤,變更已撤銷",
"Validate YAML File": "驗證YAML檔案",
"Validate Merge File": "驗證覆寫檔案",
"Validation Success": "驗證成功",
"Validation Failed": "驗證失敗",
"Service Administrator Prompt": "Clash Verge 需要管理員權限安裝系統服務",
"DNS Settings": "DNS 設定",
"DNS settings saved": "DNS 設定已儲存",
@@ -517,11 +593,14 @@
"Hosts Settings": "Hosts 設定",
"Hosts": "Hosts",
"Custom domain to IP or domain mapping": "自訂網域到 IP 或網域的映射,用逗號分隔",
"Enable Alpha Channel": "啟用 Alpha 頻道",
"Alpha versions may contain experimental features and bugs": "Alpha 版本可能內含實驗性功能和已知問題,常有不穩定的情況發生。",
"Home Settings": "首頁設定",
"Profile Card": "訂閱卡",
"Current Proxy Card": "目前代理卡",
"Network Settings Card": "網路設定卡",
"Proxy Mode Card": "代理模式卡",
"Clash Mode Card": "Clash模式卡",
"Traffic Stats Card": "流量統計卡",
"Clash Info Cards": "Clash 資訊卡",
"System Info Cards": "系統資訊卡",
@@ -538,6 +617,7 @@
"Running Mode": "執行模式",
"Sidecar Mode": "使用者模式",
"Administrator Mode": "管理員模式",
"Administrator + Service Mode": "系統管理員 + 服務模式",
"Last Check Update": "最後檢查更新",
"Click to import subscription": "點擊匯入訂閱",
"Last Update failed": "上次更新失敗",
@@ -546,7 +626,6 @@
"Unknown": "未知",
"Auto update disabled": "自動更新已停用",
"Update subscription successfully": "訂閱更新成功",
"Update failed, retrying with Clash proxy...": "訂閱更新失敗,嘗試使用 Clash 代理更新",
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
"Update failed even with Clash proxy": "使用 Clash 代理更新也失敗",
"Profile creation failed, retrying with Clash proxy...": "訂閱建立失敗,嘗試使用 Clash 代理建立",
@@ -577,11 +656,60 @@
"Completed": "檢測完成",
"Disallowed ISP": "不允許的網際網路服務供應商",
"Originals Only": "僅限原創",
"No (IP Banned By Disney+)": "不支援IP被Disney+禁止)",
"Unsupported Country/Region": "不支援的國家/地區",
"Failed (Network Connection)": "測試失敗(網路連線問題)",
"DashboardToggledTitle": "儀錶板已切換",
"DashboardToggledBody": "已透過快速鍵切換儀錶板顯示狀態",
"ClashModeChangedTitle": "Clash模式切換",
"ClashModeChangedBody": "已切換為 {mode} 模式",
"SystemProxyToggledTitle": "系統代理切換",
"SystemProxyToggledBody": "已透過快速鍵切換系統代理狀態",
"TunModeToggledTitle": "TUN模式切換",
"TunModeToggledBody": "已透過快速鍵切換TUN模式",
"LightweightModeEnteredTitle": "輕量模式",
"LightweightModeEnteredBody": "已透過快速鍵進入輕量模式",
"AppQuitTitle": "應用程式退出",
"AppQuitBody": "已透過快速鍵退出應用程式",
"AppHiddenTitle": "應用程式隱藏",
"AppHiddenBody": "已透過快速鍵隱藏應用程式視窗",
"Invalid Profile URL": "無效的訂閱網址,請輸入以 http:// 或 https:// 開頭的位址",
"Saved Successfully": "儲存成功",
"External Cors": "外部跨來源資源共享",
"Enable one-click CORS for external API. Click to toggle CORS": "設定內核跨來源存取,點擊切換跨來源資源共享是否啟用",
"External Cors Settings": "外部跨來源資源共享設定",
"External Cors Configuration": "外部跨來源資源共享設定",
"Allow private network access": "允許專用網路存取",
"Allowed Origins": "允許的來源",
"Please enter a valid url": "請輸入有效的網址",
"Add": "新增",
"Always included origins: {{urls}}": "始終包含來源:{{urls}}",
"Invalid regular expression": "無效的正規表示式",
"Copy Version": "複製Verge版本號",
"Version copied to clipboard": "Verge版本已複製到剪貼簿",
"Controller address cannot be empty": "控制器位址不能為空",
"Secret cannot be empty": "存取金鑰不能為空",
"Configuration saved successfully": "設定儲存完成",
"Failed to save configuration": "設定儲存失敗",
"Controller address copied to clipboard": "API 連接埠已複製到剪貼簿",
"Secret copied to clipboard": "API 鑰已複製到剪貼簿",
"Copy to clipboard": "點擊我複製到剪貼簿",
"Port Config": "連接埠設定",
"Configuration saved successfully": "配置儲存完成",
"Enable one-click random API port and key. Click to randomize the port and key": "開啟一鍵隨機 API 連接埠和密鑰,點擊即可隨機化連接埠和密鑰"
"Secret copied to clipboard": "API 鑰已複製到剪貼簿",
"Saving...": "儲存中...",
"Proxy node already exists in chain": "該節點已在鏈式代理表中",
"Detection timeout or failed": "檢測逾時或失敗",
"Batch Operations": "批次操作",
"Delete Selected Profiles": "刪除選取訂閱",
"Deselect All": "取消選取",
"Done": "完成",
"items": "項目",
"Select All": "全選",
"Selected": "已選取",
"Selected profiles deleted successfully": "選取的訂閱已成功刪除",
"Prefer System Titlebar": "優先使用系統標題欄",
"App Log Max Size": "應用程式日誌最大大小",
"App Log Max Count": "應用程式日誌最大數量",
"Allow Auto Update": "允許自動更新",
"Menu reorder mode": "選單排序模式",
"Unlock menu order": "解鎖選單排序",
"Lock menu order": "鎖定選單排序",
"TPROXY Port": "TPROXY 透明代理連接埠"
}

Some files were not shown because too many files have changed in this diff Show More