Compare commits

..
41 Commits
Author SHA1 Message Date
Tunglies 45fce7db73 feat: enhance autobuild workflow with pre-release tagging and promotion steps 2025-09-20 16:12:48 +08:00
Tunglies 1cf8e2384e fix: Cargo.toml dependencies option (#4805)
* chore: update cargo lock

* refactor: streamline dependency declarations in Cargo.toml

* refactor: reorder import statements and comment out Cargo.toml version update

* fix: enable Cargo version update in release script and update tauri-plugin-devtools dependency
2025-09-20 15:15:24 +08:00
Tunglies d9a5c11d6a refactor: improve code readability and consistency in proxy-chain and uri-parser utilities
refactor: add keys to icons in routers for improved rendering and performance
refactor: optimize RegExp polyfill by using Object.prototype.hasOwnProperty.call
refactor: reorder imports in chain-proxy-provider for consistency
refactor: remove unused "obfs-opts" property from IProxySnellConfig interface

refactor: reorganize imports and enhance refresh logic in app data provider

refactor: re-enable prop-types linting for better type safety in BaseDialog component

refactor: update dependencies in effect hooks for improved stability and performance
2025-09-20 11:19:36 +08:00
Tunglies 7811714f89 refactor: enhance logging system and add new development commands (#4803)
* refactor: enhance logging system and add new development commands

* refactor: add cfg-if dependency and improve logging configuration
2025-09-20 00:04:46 +08:00
Tunglies e869da8d4c refactor: remove unused HTTP-specific structs to streamline service code 2025-09-19 00:04:51 +08:00
Tunglies e414b49879 Refactor imports across multiple components for consistency and clarity
- Reorganized import statements in various components to ensure consistent ordering and grouping.
- Removed unnecessary imports and added missing ones where applicable.
- Improved readability and maintainability of the codebase by standardizing import styles.
2025-09-19 00:01:04 +08:00
Tunglies 627119bb22 Refactor imports and improve code organization across multiple components and hooks
- Consolidated and reordered imports in various files for better readability and maintainability.
- Removed unused imports and ensured consistent import styles.
- Enhanced the structure of components by grouping related imports together.
- Updated the layout and organization of hooks to streamline functionality.
- Improved the overall code quality by following best practices in import management.
2025-09-18 23:34:38 +08:00
Tunglies 74ade3ee41 refactor: update ESLint configuration and improve lint command with cache 2025-09-18 23:13:13 +08:00
Tunglies 324628dd3d refactor: replace 'let' with 'const' for better variable scoping and immutability 2025-09-18 23:07:18 +08:00
Tunglies 9d96ac0f6a feat: Integrate HTTP plugin and update IP detection to use fetch API #4712 2025-09-18 19:35:14 +08:00
Tunglies 409571f54b refactor: remove unused notification permission hook and related code 2025-09-18 19:13:23 +08:00
TianHua Liu a995a13163 chore: use jsx-runtime presets of eslint-plugin-react (#4794) 2025-09-18 19:04:12 +08:00
Tunglies 7848d6b1de refactor: window handle usage (#4788)
* refactor: Remove unused UI reset function and streamline window creation logic

* refactor: Remove debug print statements and streamline lightweight mode initialization

* fix: Ensure tray status refresh during silent startup and lightweight mode entry is independent of window creation

* refactor: Simplify window creation process and remove debug print statements
2025-09-18 10:22:43 +08:00
Tunglies 5d2e114b4d fix: Update tray menu event handling to toggle lightweight mode based on current state #4785 2025-09-18 00:46:49 +08:00
Tunglies c207516b47 refactor: clash-verge-service management (#4674)
* refactor: clash-verge-service management

* fix: correct service state checks in ProxyControlSwitches component
refactor: improve logging in service state update functions

* fix: add missing async handler for Windows and adjust logging import for macOS

* fix: streamline logging imports and add missing async handler for Windows

* refactor: remove unused useServiceStateSync hook and update imports in _layout

* refactor: remove unused useServiceStateSync import and clean up code in ProxyControlSwitches and _layout

* refactor: simplify service status checks and reduce wait time in useServiceInstaller hook

* refactor: remove unnecessary logging statements in service checks and IPC connection

* refactor: extract SwitchRow component for better code organization and readability

* refactor: enhance service state management and update related mutations in layout

* refactor: streamline core stopping logic and improve IPC connection logging

* refactor: consolidate service uninstallation logic and improve error handling

* fix: simplify conditional statements in CoreManager and service functions

* feat: add backoff dependency and implement retry strategy for IPC requests

* refactor: remove redundant Windows conditional and improve error handling in IPC tests

* test: improve error handling in IPC tests for message signing and verification

* fix: adjust IPC backoff retry parameters

* refactor: Remove service state tracking and related logic from service management

* feat: Enhance service status handling with logging and running mode updates

* fix: Improve service status handling with enhanced error logging

* fix: Ensure proper handling of service operations with error propagation

* refactor: Simplify service operation execution and enhance service status handling

* fix: Improve error message formatting in service operation execution and simplify service status retrieval

* refactor: Replace Cache with CacheProxy in multiple modules and update CacheEntry to be generic

* fix: Remove unnecessary success message from config validation

* refactor: Comment out logging statements in service version check and IPC request handling
2025-09-17 22:59:02 +08:00
Tunglies 6724f1ae35 feat: Implement caching mechanism with Cache struct and update related commands 2025-09-17 19:37:42 +08:00
Tunglies 1787d5372e fix: Update OS_PLATFORM definition to reflect the current platform 2025-09-17 16:10:22 +08:00
Tunglies 4c41144dd0 fix: Update error message to include details of accumulated startup errors 2025-09-17 16:10:01 +08:00
Tunglies 27636c848f fix: update changelog to reflect removal of hidden groups in tray node switching #4765 2025-09-17 13:39:38 +08:00
Tunglies 8060d699f0 fix: enhance prebuild script to support shorthand for force update #4777 2025-09-17 13:36:31 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f36f31a636 chore(deps): update npm dependencies (#4686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 12:45:57 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d300fac3d9 chore(deps): update cargo dependencies (#4687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 12:45:50 +08:00
Thomas 8c2262dd95 stop showing hidden groups in the tray menu (#4765)
托盘菜单不显示隐藏代理组
2025-09-17 12:45:35 +08:00
wonfen c438e916ca perf: remove system-level unlock test timeout notice 2025-09-15 22:16:13 +08:00
ZShab NibaandTunglies 0855bd4896 fix: Fix icon to RGBA/sRGB Colorspace (#4753)
* fix: Fixed icon pixel size

* fix: fix icon to RGBA/sRGB Colorspace

* chore: update UPDATELOG

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-09-15 12:29:30 +08:00
Junkai W. f2073a2f83 Add Func 链式代理 (#4624)
* 添加链式代理gui和语言支持
在Iruntime中添跟新链式代理配置方法
同时添加了cmd

* 修复读取运行时代理链配置文件bug

* t

* 完成链式代理配置构造

* 修复获取链式代理运行时配置的bug

* 完整的链式代理功能
2025-09-15 07:44:54 +08:00
Tunglies a1f468202f Revert "fix: Fixed icon pixel size (#4698)"
revert due to runtime error [[Setup]] Error: failed to process image: Format error decoding Ico: The PNG is not in RGBA format!

This reverts commit a24bf4042c.
2025-09-15 00:56:59 +08:00
ZShab Niba a24bf4042c fix: Fixed icon pixel size (#4698) 2025-09-13 10:56:07 +08:00
❤是纱雾酱哟~ 15d22b4bf6 chore(issue template): disable blank issue template in GitHub config (#4731)
- Prevent users from creating issue without a template
- Always use a template for guiding users to provide necessary information for us

Signed-off-by: Dragon1573 <49941141+Dragon1573@users.noreply.github.com>
2025-09-13 10:11:40 +08:00
Tunglies 231d264652 fix: update alpha version description in README 2025-09-12 18:45:40 +08:00
Tunglies 67ac353fd5 chore: update cron schedule for auto build workflow 2025-09-12 18:43:13 +08:00
Tunglies 1c5534ad36 fix: remove deprecated updater for alpha endpoints in tauri configuration 2025-09-11 17:14:57 +08:00
Tunglies 251678493c edition 2024 (#4702)
* feat: update Cargo.toml for 2024 edition and optimize release profiles

* feat: refactor environment variable settings for Linux and improve code organization

* Refactor conditional statements to use `&&` for improved readability

- Updated multiple files to combine nested `if let` statements using `&&` for better clarity and conciseness.
- This change enhances the readability of the code by reducing indentation levels and making the conditions more straightforward.
- Affected files include: media_unlock_checker.rs, profile.rs, clash.rs, profiles.rs, async_proxy_query.rs, core.rs, handle.rs, hotkey.rs, service.rs, timer.rs, tray/mod.rs, merge.rs, seq.rs, config.rs, proxy.rs, window.rs, general.rs, dirs.rs, i18n.rs, init.rs, network.rs, and window.rs in the resolve module.

* refactor: streamline conditional checks using `&&` for improved readability

* fix: update release profile settings for panic behavior and optimization

* fix: adjust optimization level in Cargo.toml and reorder imports in lightweight.rs
2025-09-10 09:49:06 +08:00
Tunglies ccbffa14f0 fix: replace toggle with show for main window in lightweight mode exit #4697 2025-09-09 21:25:38 +08:00
Tunglies dfc1f736af fix: resolve from lightweight cause crash (#4682)
* refactor: streamline lightweight mode handling and improve window management

* refactor: replace mutex-based window creation lock with atomic operations for improved performance

* refactor: remove startup completed event handling and simplify initialization logic

* refactor: remove conditional compilation for emit_update_event function

* refactor: simplify return statements and clean up commented code in lightweight and window manager modules

* refactor: streamline lightweight mode handling by consolidating window management calls

* refactor: prevent unnecessary window toggle when exiting lightweight mode

* refactor: reorder imports for consistency in lightweight module

* refactor: move macOS specific logging_error import for clarity
2025-09-09 18:50:24 +08:00
Tunglies c54d89a465 feat: add support for Windows ARM64 in development workflow 2025-09-09 16:55:46 +08:00
Tunglies 55b95a1985 Revert "feat: update Cargo.toml for 2024 edition and optimize release profiles (#4681)"
This reverts commit 31e3104c7f.
2025-09-08 21:48:09 +08:00
Tunglies 31e3104c7f feat: update Cargo.toml for 2024 edition and optimize release profiles (#4681)
* feat: update Cargo.toml for 2024 edition and optimize release profiles

* feat: refactor environment variable settings for Linux and improve code organization

* Refactor conditional statements to use `&&` for improved readability

- Updated multiple files to combine nested `if let` statements using `&&` for better clarity and conciseness.
- This change enhances the readability of the code by reducing indentation levels and making the conditions more straightforward.
- Affected files include: media_unlock_checker.rs, profile.rs, clash.rs, profiles.rs, async_proxy_query.rs, core.rs, handle.rs, hotkey.rs, service.rs, timer.rs, tray/mod.rs, merge.rs, seq.rs, config.rs, proxy.rs, window.rs, general.rs, dirs.rs, i18n.rs, init.rs, network.rs, and window.rs in the resolve module.

* refactor: streamline conditional checks using `&&` for improved readability
2025-09-08 13:57:32 +08:00
wonfen 58a0089b19 fix: workflow file name 2025-09-07 15:24:14 +08:00
Tunglies 043ed4cb31 feat: add known issues section and update lightweight mode handling 2025-09-07 13:00:51 +08:00
Tunglies f64c01044c feat: bump version to 2.4.3 and update changelog for macOS intel Mihomo compatibility 2025-09-07 12:06:33 +08:00
218 changed files with 6054 additions and 5345 deletions
+1
View File
@@ -1,3 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/clash_verge_rev
+66 -23
View File
@@ -3,12 +3,14 @@ name: Auto Build
on:
workflow_dispatch:
schedule:
# UTC+8 0,6,12,18
- cron: "0 16,22,4,10 * * *"
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
- cron: "0 4,10 * * *"
permissions: write-all
env:
TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild
PRE_TAG_NAME: pre-autobuild
PRE_TAG_CHANNEL: PreAutoBuild
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
@@ -71,7 +73,7 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/${{ env.PRE_TAG_NAME }}" >> $GITHUB_ENV
shell: bash
- run: |
@@ -99,7 +101,7 @@ jobs:
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [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)
#### 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)
@@ -113,11 +115,11 @@ jobs:
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
@@ -130,7 +132,7 @@ jobs:
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with:
tag_name: autobuild
tag_name: pre-autobuild
dry_run: false
autobuild-x86-windows-macos-linux:
@@ -201,7 +203,7 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Tauri build for Windows-macOS-Linux
@@ -218,14 +220,13 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tagName: ${{ env.PRE_TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
autobuild-arm-linux:
name: Autobuild ARM Linux
@@ -276,7 +277,7 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Setup for linux
@@ -351,11 +352,11 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: |
@@ -408,7 +409,7 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime
@@ -429,7 +430,6 @@ jobs:
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
- name: Rename
run: |
@@ -451,20 +451,62 @@ jobs:
Rename-Item $file.FullName $newName
}
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.PRE_TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
promote-autobuild:
name: Promote PreAutoBuild to AutoBuild
runs-on: ubuntu-latest
needs:
[
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
if: >
${{
needs.autobuild-x86-windows-macos-linux.result == 'success' &&
needs.autobuild-arm-linux.result == 'success' &&
needs.autobuild-x86-arm-windows_webview2.result == 'success'
}}
steps:
- name: Install gh CLI
run: |
sudo apt-get update
sudo apt-get install -y gh jq
- name: Download PreAutoBuild Release Assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir pre-assets
release_id=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.PRE_TAG_NAME }} | jq .id)
gh release download ${{ env.PRE_TAG_NAME }} --dir pre-assets
body=$(gh api repos/${{ github.repository }}/releases/$release_id | jq -r .body)
echo "$body" > release.txt
- name: Publish to autobuild tag
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: pre-assets/*
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
@@ -474,6 +516,7 @@ jobs:
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
promote-autobuild,
]
steps:
- name: Checkout repository
@@ -547,7 +590,7 @@ jobs:
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
- [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)
#### 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)
+10
View File
@@ -13,6 +13,11 @@ on:
required: false
type: boolean
default: true
run_windows_arm64:
description: "运行 Windows ARM64"
required: false
type: boolean
default: true
permissions: write-all
env:
@@ -40,6 +45,11 @@ jobs:
bundle: dmg
id: macos-aarch64
input: run_macos_aarch64
- os: windows-latest
target: aarch64-pc-windows-msvc
bundle: nsis
id: windows-arm64
input: run_windows_arm64
runs-on: ${{ matrix.os }}
steps:
+1
View File
@@ -10,3 +10,4 @@ scripts/_env.sh
.tool-versions
.idea
.old
.eslintcache
+5 -2
View File
@@ -59,8 +59,9 @@ You have two options for downloading the clash binary:
```shell
pnpm run prebuild
# Use '--force' to force update to the latest version
# pnpm run prebuild --force
# Use '--force' or '-f' to update both the Mihomo core version
# and the Clash Verge Rev service version to the latest available.
pnpm run prebuild --force
```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
@@ -73,6 +74,8 @@ To run the development server, use the following command:
pnpm dev
# If an app instance already exists, use a different command
pnpm dev:diff
# To using tauri built-in dev tool
pnpm dev:tauri
```
### Build the Project
+5 -5
View File
@@ -23,11 +23,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
#### 我应当怎样选择发行版
| 版本 | 特征 | 链接 |
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
| 版本 | 特征 | 链接 |
| :---------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha(废弃) | 测试发布流程。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 滚动更新版,适合测试反馈,可能存在缺陷。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
+20 -1
View File
@@ -1,3 +1,22 @@
## v2.4.3
### 🚀 优化改进
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
## v2.4.2
### ✨ 新增功能
@@ -7,7 +26,7 @@
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存呢
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
+97 -7
View File
@@ -1,16 +1,106 @@
import js from "@eslint/js";
import configPrettier from "eslint-config-prettier";
import pluginImport from "eslint-plugin-import";
import pluginPrettier from "eslint-plugin-prettier";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReactRefresh from "eslint-plugin-react-refresh";
import pluginUnusedImports from "eslint-plugin-unused-imports";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
plugins: {
js,
react: pluginReact,
"react-hooks": pluginReactHooks,
import: pluginImport,
"react-refresh": pluginReactRefresh,
"unused-imports": pluginUnusedImports,
prettier: pluginPrettier,
},
extends: [
"js/recommended",
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
pluginReact.configs.flat["jsx-runtime"],
configPrettier,
],
languageOptions: {
globals: globals.browser,
},
settings: {
react: {
version: "detect",
},
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
},
},
rules: {
// React
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
// TypeScript
"@typescript-eslint/no-explicit-any": "off",
// unused-imports 代替 no-unused-vars
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_+$",
args: "after-used",
argsIgnorePattern: "^_+$",
},
],
// Import
"import/no-unresolved": "error",
"import/order": [
"warn",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
// 其他常见
"prefer-const": "warn",
"no-case-declarations": "error",
"no-fallthrough": "error",
"no-empty": ["warn", { allowEmptyCatch: true }],
// Prettier 格式化问题
"prettier/prettier": "warn",
},
},
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
]);
+26 -21
View File
@@ -1,11 +1,12 @@
{
"name": "clash-verge",
"version": "2.4.2",
"version": "2.4.3",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"dev:tauri": "cross-env RUST_BACKTRACE=full tauri dev -f tauri-dev",
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri",
@@ -24,7 +25,7 @@
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint src",
"lint": "eslint -c eslint.config.ts --cache src",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
@@ -38,27 +39,25 @@
"@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.1",
"@mui/x-data-grid": "^8.11.2",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.5",
"axios": "^1.11.0",
"cli-color": "^2.0.4",
"axios": "^1.12.2",
"dayjs": "1.11.18",
"foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.5.2",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"monaco-editor": "^0.53.0",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5",
"react": "19.1.1",
@@ -68,11 +67,9 @@
"react-i18next": "15.7.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.2",
"react-router-dom": "7.9.1",
"react-virtuoso": "^4.14.0",
"sockette": "^2.0.6",
"swr": "^2.3.6",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
},
@@ -82,28 +79,36 @@
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.12",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cli-color": "^2.0.4",
"commander": "^14.0.1",
"cross-env": "^10.0.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-unused-imports": "^4.2.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
"https-proxy-agent": "^7.0.6",
"jiti": "^2.5.1",
"meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2",
"path": "^0.12.7",
"prettier": "^3.6.2",
"process": "^0.11.10",
"sass": "^1.92.1",
"tar": "^7.4.3",
"terser": "^5.44.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vite": "^7.1.4",
"typescript-eslint": "^8.44.0",
"vite": "^7.1.5",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0"
},
+802 -263
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -12,7 +12,7 @@ import { glob } from "glob";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force");
const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32",
@@ -43,7 +43,8 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" ? arg2 : arg1;
let target;
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
@@ -64,7 +65,7 @@ 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-v2",
"darwin-x64": "mihomo-darwin-amd64-v1",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
+2 -2
View File
@@ -29,10 +29,10 @@
* Errors are logged and the process exits with code 1 on failure.
*/
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
import { execSync } from "child_process";
/**
* 获取当前 git 短 commit hash
+546 -504
View File
File diff suppressed because it is too large Load Diff
+35 -24
View File
@@ -1,12 +1,12 @@
[package]
name = "clash-verge"
version = "2.4.2"
version = "2.4.3"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
edition = "2021"
edition = "2024"
build = "build.rs"
[package.metadata.bundle]
@@ -22,12 +22,11 @@ dirs = "6.0"
open = "5.3.2"
log = "0.4.28"
dunce = "1.0.5"
log4rs = "1.4.0"
nanoid = "0.4"
chrono = "0.4.41"
chrono = "0.4.42"
sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.143"
serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
once_cell = "1.21.3"
port_scanner = "0.1.5"
@@ -40,7 +39,7 @@ tokio = { version = "1.47.1", features = [
"time",
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
serde = { version = "1.0.225", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
@@ -58,9 +57,8 @@ tauri-plugin-fs = "2.4.2"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0"
zip = "5.0.0"
zip = "5.1.1"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
@@ -73,15 +71,21 @@ hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc2"
kode-bridge = "0.2.1"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
"parking_lot",
] }
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2"
flexi_logger = "0.31.2"
cfg-if = "1.0.3"
nu-ansi-term = { version = "0.50.1", optional = true }
console-subscriber = { version = "0.4.1", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
[target.'cfg(windows)'.dependencies]
@@ -113,31 +117,38 @@ tauri-plugin-updater = "2.9.0"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = []
verge-dev = ["nu-ansi-term"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
[profile.release]
panic = "abort"
codegen-units = 1
codegen-units = 16
lto = true
opt-level = "s"
opt-level = 2
debug = false
strip = true
overflow-checks = false
rpath = false
[profile.dev]
incremental = true
codegen-units = 256 # 增加编译单元,提升编译速度
opt-level = 0 # 禁用优化,进一步提升编译速度
debug = true # 保留调试信息
strip = false # 不剥离符号,保留调试信息
codegen-units = 64
opt-level = 0
debug = true
strip = "none"
overflow-checks = true
lto = false
rpath = false
[profile.fast-release]
inherits = "release" # 继承 release 的配置
panic = "abort" # 与 release 相同
codegen-units = 256 # 增加编译单元,提升编译速度
lto = false # 禁用 LTO,提升编译速度
opt-level = 0 # 禁用优化,大幅提升编译速度
debug = true # 保留调试信息
strip = false # 不剥离符号,保留调试信息
inherits = "release"
incremental = true
codegen-units = 64
lto = false
opt-level = 0
debug = true
strip = false
[lib]
name = "app_lib"
+1 -1
View File
@@ -1,4 +1,4 @@
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;
// 业务模型 & Draft
+7 -1
View File
@@ -18,6 +18,12 @@
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:window:allow-set-theme",
"notification:default"
"notification:default",
"http:default",
"http:allow-fetch",
{
"identifier": "http:default",
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+22 -13
View File
@@ -1,24 +1,25 @@
// use crate::utils::logging::Type;
// use crate::{logging, singleton};
use crate::singleton;
use anyhow::Result;
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::OnceCell;
pub struct CacheEntry {
pub value: Arc<Value>,
pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
pub struct CacheEntry<T> {
pub value: Arc<T>,
pub expires_at: Instant,
}
pub struct ProxyRequestCache {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry>>>>,
pub struct Cache<T> {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
}
impl ProxyRequestCache {
impl<T> Cache<T> {
fn new() -> Self {
ProxyRequestCache {
Cache {
map: DashMap::new(),
}
}
@@ -27,10 +28,11 @@ impl ProxyRequestCache {
format!("{prefix}:{id}")
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<T>
where
F: Fn() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Value> + Send + 'static,
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
T: Send + Sync + 'static,
{
loop {
let now = Instant::now();
@@ -81,6 +83,10 @@ impl ProxyRequestCache {
}
}
// pub fn clean_key(&self, key: &str) {
// self.map.remove(key);
// }
// TODO
pub fn clean_default_keys(&self) {
// logging!(info, Type::Cache, "Cleaning proxies keys");
@@ -98,5 +104,8 @@ impl ProxyRequestCache {
}
}
// Use singleton macro
singleton!(ProxyRequestCache, INSTANCE);
pub type CacheService = Cache<Result<String>>;
pub type CacheProxy = Cache<Value>;
singleton!(Cache<Value>, PROXY_INSTANCE);
singleton!(Cache<Result<String>>, SERVICE_INSTANCE);
-8
View File
@@ -235,11 +235,3 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(())
}
/// 重置UI就绪状态
#[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "重置UI就绪状态");
crate::utils::resolve::ui::reset_ui_ready();
Ok(())
}
+7 -7
View File
@@ -1,14 +1,14 @@
use super::CmdResult;
use crate::{
cache::CacheProxy,
config::Config,
core::{handle, CoreManager},
core::{CoreManager, handle},
};
use crate::{
config::*,
feat,
ipc::{self, IpcManager},
logging,
state::proxy::ProxyRequestCache,
utils::logging::Type,
wrap_err,
};
@@ -166,7 +166,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{handle, CoreManager},
core::{CoreManager, handle},
utils::dirs,
};
@@ -317,8 +317,8 @@ pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| {
@@ -333,8 +333,8 @@ pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
cache.map.remove(&key);
get_clash_config().await
}
+26 -30
View File
@@ -618,20 +618,20 @@ async fn check_netflix(client: &Client) -> UnlockItem {
{
Ok(response) => {
// 检查重定向位置
if let Some(location) = response.headers().get("location") {
if let Ok(location_str) = location.to_str() {
// 解析位置获取区域
let parts: Vec<&str> = location_str.split('/').collect();
if parts.len() >= 4 {
let region_code = parts[3].split('-').next().unwrap_or("unknown");
let emoji = country_code_to_emoji(region_code);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{region_code}")),
check_time: Some(get_local_date_string()),
};
}
if let Some(location) = response.headers().get("location")
&& let Ok(location_str) = location.to_str()
{
// 解析位置获取区域
let parts: Vec<&str> = location_str.split('/').collect();
if parts.len() >= 4 {
let region_code = parts[3].split('-').next().unwrap_or("unknown");
let emoji = country_code_to_emoji(region_code);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{region_code}")),
check_time: Some(get_local_date_string()),
};
}
}
// 如果没有重定向,假设是美国
@@ -691,22 +691,18 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
match response.json::<serde_json::Value>().await {
Ok(data) => {
// 尝试从数据中提取区域信息
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) {
if !targets.is_empty() {
if let Some(location) = targets[0].get("location") {
if let Some(country) =
location.get("country").and_then(|c| c.as_str())
{
let emoji = country_code_to_emoji(country);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()),
};
}
}
}
if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
&& !targets.is_empty()
&& let Some(location) = targets[0].get("location")
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
{
let emoji = country_code_to_emoji(country);
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()),
};
}
// 如果无法解析区域信息
+1 -1
View File
@@ -1,5 +1,5 @@
use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
use crate::process::AsyncHandler;
use crate::wrap_err;
use network_interface::NetworkInterface;
+95 -96
View File
@@ -1,13 +1,14 @@
use super::CmdResult;
use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
profiles::{
profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe,
},
profiles_append_item_safe, Config, IProfiles, PrfItem, PrfOption,
profiles_append_item_safe,
},
core::{handle, timer::Timer, tray::Tray, CoreManager},
core::{CoreManager, handle, timer::Timer, tray::Tray},
feat, logging,
process::AsyncHandler,
ret_err,
@@ -290,109 +291,106 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() {
if current_profile.as_ref() != Some(new_profile) {
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
if let Some(new_profile) = profiles.current.as_ref()
&& current_profile.as_ref() != Some(new_profile)
{
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", 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));
path.ok()
} else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
// 获取目标配置文件路径
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));
path.ok()
} else {
None
}
}
};
// 如果获取到文件路径,检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None
}
}
};
// 超时保护
let file_read_result = tokio::time::timeout(
Duration::from_secs(5),
tokio::fs::read_to_string(&file_path),
)
.await;
// 如果获取到文件路径,检查YAML语法
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
}
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;
// 超时保护
let file_read_result = tokio::time::timeout(
Duration::from_secs(5),
tokio::fs::read_to_string(&file_path),
)
.await;
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
true,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&error_msg,
);
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
&error_msg,
);
return Ok(false);
}
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, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
true,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
&error_msg,
);
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
&error_msg,
);
return Ok(false);
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
&error_msg,
);
return Ok(false);
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
&error_msg,
);
return Ok(false);
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
return Ok(false);
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
&error_msg,
);
return Ok(false);
}
}
}
@@ -663,8 +661,9 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command]
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let file = {
wrap_err!(profiles.latest_ref().get_item(&index))?
wrap_err!(profiles_ref.get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
+11 -9
View File
@@ -2,10 +2,10 @@ use tauri::Emitter;
use super::CmdResult;
use crate::{
cache::CacheProxy,
core::{handle::Handle, tray::Tray},
ipc::IpcManager,
logging,
state::proxy::ProxyRequestCache,
utils::logging::Type,
};
use std::time::Duration;
@@ -15,8 +15,8 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
@@ -32,16 +32,16 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key);
get_proxies().await
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("providers", "default");
let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
@@ -76,6 +76,7 @@ pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
match IpcManager::global().update_proxy(&group, &proxy).await {
Ok(_) => {
// println!("Proxy updated successfully: {} -> {}", group,proxy);
logging!(
info,
Type::Cmd,
@@ -84,8 +85,8 @@ pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()
proxy
);
let cache = crate::state::proxy::ProxyRequestCache::global();
let key = crate::state::proxy::ProxyRequestCache::make_key("proxies", "default");
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key);
if let Err(e) = Tray::global().update_menu().await {
@@ -107,6 +108,7 @@ pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()
Ok(())
}
Err(e) => {
println!("1111111111111111");
logging!(
error,
Type::Cmd,
+95 -6
View File
@@ -1,5 +1,5 @@
use super::CmdResult;
use crate::{config::*, wrap_err};
use crate::{config::*, core::CoreManager, log_err, wrap_err};
use anyhow::Context;
use serde_yaml_ng::Mapping;
use std::collections::HashMap;
@@ -15,12 +15,14 @@ pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = runtime.config.as_ref();
wrap_err!(config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(
|config| serde_yaml_ng::to_string(config).context("failed to convert config to yaml")
))
wrap_err!(
config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(|config| serde_yaml_ng::to_string(config)
.context("failed to convert config to yaml"))
)
}
/// 获取运行时存在的键
@@ -34,3 +36,90 @@ pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
}
/// 读取运行时链式代理配置
#[tauri::command]
pub async fn get_runtime_proxy_chain_config() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = wrap_err!(
runtime
.config
.as_ref()
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
)?;
if let (
Some(serde_yaml_ng::Value::Sequence(proxies)),
Some(serde_yaml_ng::Value::Sequence(proxy_groups)),
) = (config.get("proxies"), config.get("proxy-groups"))
{
let mut proxy_name = None;
let mut proxies_chain = Vec::new();
let proxy_chain_groups = proxy_groups
.iter()
.filter_map(
|proxy_group| match proxy_group.get("name").and_then(|n| n.as_str()) {
Some("proxy_chain") => {
if let Some(serde_yaml_ng::Value::Sequence(ps)) = proxy_group.get("proxies")
&& let Some(x) = ps.first()
{
proxy_name = Some(x); //插入出口节点名字
}
Some(proxy_group.to_owned())
}
_ => None,
},
)
.collect::<Vec<serde_yaml_ng::Value>>();
while let Some(proxy) = proxies.iter().find(|proxy| {
if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy {
proxy_map.get("name") == proxy_name && proxy_map.get("dialer-proxy").is_some()
} else {
false
}
}) {
proxies_chain.push(proxy.to_owned());
proxy_name = proxy.get("dialer-proxy");
}
if let Some(entry_proxy) = proxies.iter().find(|proxy| proxy.get("name") == proxy_name) {
proxies_chain.push(entry_proxy.to_owned());
}
proxies_chain.reverse();
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
config.insert("proxies".to_string(), proxies_chain);
config.insert("proxy-groups".to_string(), proxy_chain_groups);
wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed"))
} else {
wrap_err!(Err(anyhow::anyhow!(
"failed to get proxies or proxy-groups".to_string()
)))
}
}
/// 更新运行时链式代理配置
#[tauri::command]
pub async fn update_proxy_chain_config_in_runtime(
proxy_chain_config: Option<serde_yaml_ng::Value>,
) -> CmdResult<()> {
{
let runtime = Config::runtime().await;
let mut draft = runtime.draft_mut();
draft.update_proxy_chain_config(proxy_chain_config);
drop(draft);
runtime.apply();
}
// 生成新的运行配置文件并通知 Clash 核心重新加载
let run_path = wrap_err!(Config::generate_file(ConfigType::Run).await)?;
log_err!(CoreManager::global().put_configs_force(run_path).await);
Ok(())
}
+18 -16
View File
@@ -1,45 +1,47 @@
use super::CmdResult;
use crate::{
core::{service, CoreManager},
core::{
CoreManager,
service::{self, SERVICE_MANAGER, ServiceStatus},
},
utils::i18n::t,
};
use anyhow::Result;
async fn execute_service_operation_sync<F, Fut, E>(service_op: F, op_type: &str) -> CmdResult
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), E>>,
E: ToString + std::fmt::Debug,
{
if let Err(e) = service_op().await {
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string());
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
if let Err(e) = SERVICE_MANAGER
.lock()
.await
.handle_service_status(&status)
.await
{
let emsg = format!("{} Service failed: {}", op_type, e);
return Err(t(emsg.as_str()).await);
}
if CoreManager::global().restart_core().await.is_err() {
let emsg = format!("{} {} failed", "Restart", "Core");
return Err(t(emsg.as_str()).await);
let emsg = "Restart Core failed";
return Err(t(emsg).await);
}
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
execute_service_operation_sync(service::install_service, "Install").await
execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
execute_service_operation_sync(service::uninstall_service, "Uninstall").await
execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await
}
#[tauri::command]
pub async fn reinstall_service() -> CmdResult {
execute_service_operation_sync(service::reinstall_service, "Reinstall").await
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await
}
#[tauri::command]
pub async fn repair_service() -> CmdResult {
execute_service_operation_sync(service::force_reinstall_service, "Repair").await
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await
}
#[tauri::command]
+1 -1
View File
@@ -1,6 +1,6 @@
use super::CmdResult;
use crate::{
core::{handle, CoreManager},
core::{CoreManager, handle},
logging,
module::sysinfo::PlatformSpecification,
utils::logging::Type,
+8 -8
View File
@@ -25,17 +25,17 @@ impl IClashTemp {
match map_result {
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
if let Some(value) = template.0.get(key) {
map.insert(key.clone(), value.clone());
}
if !map.contains_key(key)
&& let Some(value) = template.0.get(key)
{
map.insert(key.clone(), value.clone());
}
});
// 确保 secret 字段存在且不为空
if let Some(Value::String(s)) = map.get_mut("secret") {
if s.is_empty() {
*s = "set-your-secret".to_string();
}
if let Some(Value::String(s)) = map.get_mut("secret")
&& s.is_empty()
{
*s = "set-your-secret".to_string();
}
Self(Self::guard(map))
}
+7 -5
View File
@@ -1,14 +1,14 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
config::{profiles_append_item_safe, PrfItem},
core::{handle, CoreManager},
config::{PrfItem, profiles_append_item_safe},
core::{CoreManager, handle},
enhance, logging,
utils::{dirs, help, logging::Type},
};
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use std::path::PathBuf;
use tokio::sync::OnceCell;
use tokio::time::{sleep, Duration};
use tokio::time::{Duration, sleep};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -101,7 +101,9 @@ impl Config {
Some(("config_validate::boot_error", error_msg))
} else {
logging!(info, Type::Config, true, "配置验证成功");
Some(("config_validate::success", String::new()))
// 前端没有必要知道验证成功的消息,也没有事件驱动
// Some(("config_validate::success", String::new()))
None
}
}
Err(err) => {
+2 -2
View File
@@ -1,9 +1,9 @@
use crate::utils::dirs::get_encryption_key;
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
aead::{Aead, KeyInit},
};
use base64::{engine::general_purpose::STANDARD, Engine};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const NONCE_LENGTH: usize = 12;
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::utils::{
network::{NetworkManager, ProxyType},
tmpl,
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use std::{fs, time::Duration};
+162 -168
View File
@@ -1,10 +1,10 @@
use super::{prfitem::PrfItem, PrfOption};
use super::{PrfOption, prfitem::PrfItem};
use crate::{
logging_error,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use std::collections::HashSet;
@@ -88,12 +88,12 @@ impl IProfiles {
self.items = Some(vec![]);
}
if let Some(current) = patch.current {
if let Some(items) = self.items.as_ref() {
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
if let Some(current) = patch.current
&& let Some(items) = self.items.as_ref()
{
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}
@@ -287,24 +287,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the merge index
for (i, _) in items.iter().enumerate() {
@@ -313,24 +313,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = merge_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = merge_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the script index
for (i, _) in items.iter().enumerate() {
@@ -339,24 +339,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = script_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = script_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the rules index
for (i, _) in items.iter().enumerate() {
@@ -365,24 +365,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = rules_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = rules_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the proxies index
for (i, _) in items.iter().enumerate() {
@@ -391,24 +391,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = proxies_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = proxies_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// get the groups index
for (i, _) in items.iter().enumerate() {
@@ -417,24 +417,24 @@ impl IProfiles {
break;
}
}
if let Some(index) = groups_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
if let Some(index) = groups_index
&& let Some(file) = items.remove(index).file
{
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
});
}
}
});
}
// delete the original uid
if current == uid {
@@ -595,25 +595,25 @@ impl IProfiles {
total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& Self::is_profile_file(file_name)
{
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
// 检查是否为活跃文件
if !active_files.contains(file_name) {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
// 检查是否为活跃文件
if !active_files.contains(file_name) {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
}
}
@@ -659,50 +659,44 @@ impl IProfiles {
}
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype {
if itype == "remote" || itype == "local" {
if let Some(option) = &item.option {
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge {
if let Ok(merge_item) = self.get_item(merge_uid) {
if let Some(file) = &merge_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(itype) = &item.itype
&& (itype == "remote" || itype == "local")
&& let Some(option) = &item.option
{
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file.clone());
}
if let Some(script_uid) = &option.script {
if let Ok(script_item) = self.get_item(script_uid) {
if let Some(file) = &script_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file.clone());
}
if let Some(rules_uid) = &option.rules {
if let Ok(rules_item) = self.get_item(rules_uid) {
if let Some(file) = &rules_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file.clone());
}
if let Some(proxies_uid) = &option.proxies {
if let Ok(proxies_item) = self.get_item(proxies_uid) {
if let Some(file) = &proxies_item.file {
active_files.insert(file.clone());
}
}
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file.clone());
}
if let Some(groups_uid) = &option.groups {
if let Ok(groups_item) = self.get_item(groups_uid) {
if let Some(file) = &groups_item.file {
active_files.insert(file.clone());
}
}
}
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file.clone());
}
}
}
+135
View File
@@ -46,4 +46,139 @@ impl IRuntime {
}
}
}
//跟新链式代理配置文件
/// {
/// "proxies":[
/// {
/// name : 入口节点,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// },
/// {
/// name : hop_node_1_xxxx,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// dialer-proxy : "入口节点"
/// },
/// {
/// name : 出口节点,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// dialer-proxy : "hop_node_1_xxxx"
/// }
/// ],
/// "proxy-groups" : [
/// {
/// name : "proxy_chain",
/// type: "select",
/// proxies ["出口节点"]
/// }
/// ]
/// }
///
/// 传入none 为删除
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
if let Some(config) = self.config.as_mut() {
// 获取 默认第一的代理组的名字
let proxy_group_name =
if let Some(Value::Sequence(proxy_groups)) = config.get("proxy-groups") {
if let Some(Value::Mapping(proxy_group)) = proxy_groups.first() {
if let Some(Value::String(proxy_group_name)) = proxy_group.get("name") {
proxy_group_name.to_string()
} else {
"".to_string()
}
} else {
"".to_string()
}
} else {
"".to_string()
};
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
proxies.iter_mut().for_each(|proxy| {
if let Some(proxy) = proxy.as_mapping_mut()
&& proxy.get("dialer-proxy").is_some()
{
proxy.remove("dialer-proxy");
}
});
}
// 清除proxy_chain代理组
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.retain(|proxy_group| {
!matches!(proxy_group.get("name").and_then(|n| n.as_str()), Some(name) if name== "proxy_chain")
});
}
// 清除rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules") {
rules.retain(|rule| rule.as_str() != Some("MATCH,proxy_chain"));
rules.push(Value::String(format!("MATCH,{}", proxy_group_name)));
}
// some 则写入新配置
// 给proxy添加dialer-proxy字段
// 第一个proxy不添加dialer-proxy
// 然后第二个开始,dialer-proxy为上一个元素的name
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config {
let mut proxy_chain_group = Mapping::new();
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
if let Some(Value::Mapping(proxy)) = proxies
.iter_mut()
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
{
if i != 0
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
{
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
}
if i == dialer_proxies.len() - 1 {
// 添加proxy-groups
proxy_chain_group
.insert("name".into(), Value::String("proxy_chain".into()));
proxy_chain_group
.insert("type".into(), Value::String("select".into()));
proxy_chain_group.insert(
"proxies".into(),
Value::Sequence(vec![dialer_proxy.to_owned()]),
);
}
}
}
}
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.push(Value::Mapping(proxy_chain_group));
}
// 添加rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules")
&& let Ok(rule) = serde_yaml_ng::to_value("MATCH,proxy_chain")
{
rules.retain(|rule| {
rule.as_str() != Some(&format!("MATCH,{}", proxy_group_name))
});
rules.push(rule);
// *rules = vec![rule];
}
}
}
}
}
+1 -8
View File
@@ -1,5 +1,5 @@
use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging,
utils::{dirs, help, i18n, logging::Type},
};
@@ -203,9 +203,6 @@ pub struct IVerge {
/// 启用外部控制器
pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -407,7 +404,6 @@ impl IVerge {
auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(false),
home_cards: None,
service_state: None,
enable_external_controller: Some(false),
..Self::default()
}
@@ -495,7 +491,6 @@ impl IVerge {
patch!(auto_light_weight_minutes);
patch!(enable_dns_settings);
patch!(home_cards);
patch!(service_state);
patch!(enable_external_controller);
}
@@ -592,7 +587,6 @@ pub struct IVergeResponse {
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
impl From<IVerge> for IVergeResponse {
@@ -664,7 +658,6 @@ impl From<IVerge> for IVergeResponse {
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
enable_external_controller: verge.enable_external_controller,
service_state: verge.service_state,
}
}
}
+81 -80
View File
@@ -2,7 +2,7 @@
use crate::process::AsyncHandler;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
#[cfg(target_os = "linux")]
use anyhow::anyhow;
@@ -87,7 +87,7 @@ impl AsyncProxyQuery {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -209,13 +209,13 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和GNOME设置
// 首先检查环境变量
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
if !auto_proxy.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: auto_proxy,
});
}
if let Ok(auto_proxy) = std::env::var("auto_proxy")
&& !auto_proxy.is_empty()
{
return Ok(AsyncAutoproxy {
enable: true,
url: auto_proxy,
});
}
// 尝试使用 gsettings 获取 GNOME 代理设置
@@ -224,31 +224,31 @@ impl AsyncProxyQuery {
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
if mode.contains("auto") {
// 获取 PAC URL
let pac_output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
.output()
.await;
if let Ok(output) = output
&& output.status.success()
{
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
if mode.contains("auto") {
// 获取 PAC URL
let pac_output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
.output()
.await;
if let Ok(pac_output) = pac_output {
if pac_output.status.success() {
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if let Ok(pac_output) = pac_output
&& pac_output.status.success()
{
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if !pac_url.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
});
}
}
if !pac_url.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
});
}
}
}
@@ -271,7 +271,7 @@ impl AsyncProxyQuery {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -402,10 +402,10 @@ impl AsyncProxyQuery {
http_host = host_part.trim().to_string();
}
} else if line.contains("HTTPPort") {
if let Some(port_part) = line.split(':').nth(1) {
if let Ok(port) = port_part.trim().parse::<u16>() {
http_port = port;
}
if let Some(port_part) = line.split(':').nth(1)
&& let Ok(port) = port_part.trim().parse::<u16>()
{
http_port = port;
}
} else if line.contains("ExceptionsList") {
// 解析异常列表
@@ -431,16 +431,16 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和桌面环境设置
// 首先检查环境变量
if let Ok(http_proxy) = std::env::var("http_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
return Ok(proxy_info);
}
if let Ok(http_proxy) = std::env::var("http_proxy")
&& let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy)
{
return Ok(proxy_info);
}
if let Ok(https_proxy) = std::env::var("https_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
return Ok(proxy_info);
}
if let Ok(https_proxy) = std::env::var("https_proxy")
&& let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy)
{
return Ok(proxy_info);
}
// 尝试使用 gsettings 获取 GNOME 代理设置
@@ -449,45 +449,46 @@ impl AsyncProxyQuery {
.output()
.await;
if let Ok(mode_output) = mode_output {
if mode_output.status.success() {
let mode = String::from_utf8_lossy(&mode_output.stdout)
.trim()
.to_string();
if mode.contains("manual") {
// 获取HTTP代理设置
let host_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "host"])
.output()
.await;
if let Ok(mode_output) = mode_output
&& mode_output.status.success()
{
let mode = String::from_utf8_lossy(&mode_output.stdout)
.trim()
.to_string();
if mode.contains("manual") {
// 获取HTTP代理设置
let host_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "host"])
.output()
.await;
let port_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "port"])
.output()
.await;
let port_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "port"])
.output()
.await;
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
if host_output.status.success() && port_output.status.success() {
let host = String::from_utf8_lossy(&host_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result)
&& host_output.status.success()
&& port_output.status.success()
{
let host = String::from_utf8_lossy(&host_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
let port = String::from_utf8_lossy(&port_output.stdout)
.trim()
.parse::<u16>()
.unwrap_or(8080);
let port = String::from_utf8_lossy(&port_output.stdout)
.trim()
.parse::<u16>()
.unwrap_or(8080);
if !host.is_empty() {
return Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: String::new(),
});
}
}
if !host.is_empty() {
return Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: String::new(),
});
}
}
}
+69 -263
View File
@@ -1,13 +1,13 @@
#[cfg(target_os = "windows")]
use crate::AsyncHandler;
use crate::{
config::*,
core::{
handle,
service::{self},
service::{self, SERVICE_MANAGER, ServiceStatus},
},
ipc::IpcManager,
logging, logging_error,
process::AsyncHandler,
singleton_lazy,
logging, logging_error, singleton_lazy,
utils::{
dirs,
help::{self},
@@ -19,12 +19,12 @@ use chrono::Local;
use parking_lot::Mutex;
use std::{
fmt,
fs::{create_dir_all, File},
fs::{File, create_dir_all},
io::Write,
path::PathBuf,
sync::Arc,
};
use tauri_plugin_shell::{process::CommandChild, ShellExt};
use tauri_plugin_shell::{ShellExt, process::CommandChild};
#[derive(Debug)]
pub struct CoreManager {
@@ -384,8 +384,6 @@ impl CoreManager {
return Ok((true, String::new()));
}
logging!(info, Type::Config, true, "开始更新配置");
// 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate().await?;
@@ -393,9 +391,8 @@ impl CoreManager {
// 2. 验证配置
match self.validate_config().await {
Ok((true, _)) => {
logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "生成运行时配置");
logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run).await?;
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
Ok((true, "something".into()))
@@ -467,18 +464,18 @@ impl CoreManager {
Ok((pids, process_name)) => {
for pid in pids {
// 跳过当前管理的进程
if let Some(current) = current_pid {
if pid == current {
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
continue;
}
if let Some(current) = current_pid
&& pid == current
{
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
continue;
}
pids_to_kill.push((pid, process_name.clone()));
}
@@ -527,7 +524,7 @@ impl CoreManager {
use std::mem;
use winapi::um::handleapi::CloseHandle;
use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
};
use winapi::um::winnt::HANDLE;
@@ -703,7 +700,7 @@ impl CoreManager {
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = AsyncHandler::spawn_blocking(move || -> Result<bool> {
AsyncHandler::spawn_blocking(move || -> Result<bool> {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() {
@@ -719,9 +716,7 @@ impl CoreManager {
Ok(exit_code == 259)
}
})
.await?;
result
.await?
}
#[cfg(not(windows))]
@@ -736,7 +731,8 @@ impl CoreManager {
}
async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by sidecar");
logging!(info, Type::Core, true, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run).await?;
let app_handle = handle::Handle::global()
.app_handle()
@@ -765,21 +761,19 @@ impl CoreManager {
])
.spawn()?;
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
logging!(
error,
Type::Core,
true,
"[Sidecar] Failed to write stdout to file: {}",
e
);
}
}
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event
&& let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line))
{
logging!(
error,
Type::Core,
true,
"[Sidecar] Failed to write stdout to file: {}",
e
);
}
});
}
let pid = child.pid();
logging!(
@@ -794,7 +788,7 @@ impl CoreManager {
Ok(())
}
fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by sidecar");
logging!(info, Type::Core, true, "Stopping core by sidecar");
if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid();
@@ -814,14 +808,14 @@ impl CoreManager {
impl CoreManager {
async fn start_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by service");
logging!(info, Type::Core, true, "Running core by service");
let config_file = &Config::generate_file(ConfigType::Run).await?;
service::run_core_by_service(config_file).await?;
self.set_running_mode(RunningMode::Service);
Ok(())
}
async fn stop_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by service");
logging!(info, Type::Core, true, "Stopping core by service");
service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
@@ -837,58 +831,9 @@ impl Default for CoreManager {
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
impl CoreManager {
// 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await {
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
if let Err(e) = service::reinstall_service().await {
logging!(
warn,
Type::Core,
true,
"服务重装失败 during attempt_service_init: {}",
e
);
return Err(e);
}
// 如果重装成功,还需要尝试启动服务
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
}
if let Err(e) = self.start_core_by_service().await {
logging!(
warn,
Type::Core,
true,
"通过服务启动核心失败 during attempt_service_init: {}",
e
);
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
let mut state = service::ServiceState::get().await;
if !state.prefer_sidecar {
state.prefer_sidecar = true;
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
if let Err(save_err) = state.save().await {
logging!(
error,
Type::Core,
true,
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
save_err
);
}
}
return Err(e);
}
Ok(())
}
pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core");
logging!(info, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
@@ -901,155 +846,11 @@ impl CoreManager {
);
}
let mut core_started_successfully = false;
// 使用简化的启动流程
logging!(info, Type::Core, true, "开始核心初始化");
self.start_core().await?;
if service::is_service_available().await.is_ok() {
logging!(
info,
Type::Core,
true,
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
);
match self.attempt_service_init().await {
Ok(_) => {
logging!(info, Type::Core, true, "服务模式成功启动核心");
core_started_successfully = true;
}
Err(_err) => {
logging!(
warn,
Type::Core,
true,
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
);
}
}
} else {
logging!(
info,
Type::Core,
true,
"服务初始不可用 (is_service_available 调用失败)"
);
}
if !core_started_successfully {
logging!(
info,
Type::Core,
true,
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
);
let service_state = service::ServiceState::get().await;
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
);
self.start_core_by_sidecar().await?;
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
} else {
let has_service_install_record = service_state.last_install_time > 0;
if !has_service_install_record {
logging!(
info,
Type::Core,
true,
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
);
match service::install_service().await {
Ok(_) => {
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
let mut new_state = service::ServiceState::default();
new_state.record_install();
new_state.prefer_sidecar = false;
new_state.save().await?;
if service::is_service_available().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
if self.start_core_by_service().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务启动成功");
} else {
logging!(
warn,
Type::Core,
true,
"新安装的服务启动失败,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get().await;
final_state.prefer_sidecar = true;
final_state.last_error =
Some("Newly installed service failed to start".to_string());
final_state.save().await?;
self.start_core_by_sidecar().await?;
}
} else {
logging!(
warn,
Type::Core,
true,
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get().await;
final_state.prefer_sidecar = true;
final_state.last_error = Some(
"Newly installed service not immediately available/connectable"
.to_string(),
);
final_state.save().await?;
self.start_core_by_sidecar().await?;
}
}
Err(err) => {
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
let new_state = service::ServiceState {
last_error: Some(err.to_string()),
prefer_sidecar: true,
..Default::default()
};
new_state.save().await?;
self.start_core_by_sidecar().await?;
}
}
} else {
// 有安装记录,服务未成功启动,且初始不偏好sidecar
// 这意味着服务之前可能可用,但 attempt_service_init 失败了(并应已设置 prefer_sidecar),
// 或者服务初始不可用,无偏好,有记录。应强制使用 sidecar。
logging!(
info,
Type::Core,
true,
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
);
let mut final_state = service::ServiceState::get().await;
if !final_state.prefer_sidecar {
logging!(
warn,
Type::Core,
true,
"prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true"
);
final_state.prefer_sidecar = true;
final_state.last_error =
Some(final_state.last_error.unwrap_or_else(|| {
"Service startup failed or unavailable before sidecar fallback"
.to_string()
}));
final_state.save().await?;
}
self.start_core_by_sidecar().await?;
}
}
}
logging!(trace, Type::Core, "Initied core logic completed");
// #[cfg(target_os = "macos")]
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
logging!(info, Type::Core, true, "核心初始化完成");
Ok(())
}
@@ -1063,31 +864,32 @@ impl CoreManager {
(*guard).clone()
}
pub async fn prestart_core(&self) -> Result<()> {
SERVICE_MANAGER.lock().await.refresh().await?;
match SERVICE_MANAGER.lock().await.current() {
ServiceStatus::Ready => {
self.set_running_mode(RunningMode::Service);
}
_ => {
self.set_running_mode(RunningMode::Sidecar);
}
}
Ok(())
}
/// 启动核心
pub async fn start_core(&self) -> Result<()> {
if service::is_service_available().await.is_ok() {
if service::check_service_needs_reinstall().await {
service::reinstall_service().await?;
self.prestart_core().await?;
match self.get_running_mode() {
RunningMode::Service => {
logging_error!(Type::Core, true, self.start_core_by_service().await);
}
RunningMode::NotRunning | RunningMode::Sidecar => {
logging_error!(Type::Core, true, self.start_core_by_sidecar().await);
}
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
self.start_core_by_service().await?;
return Ok(());
};
// 服务不可用,检查用户偏好
let service_state = service::ServiceState::get().await;
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"服务不可用,根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar().await?;
} else {
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
self.start_core_by_sidecar().await?;
}
Ok(())
}
@@ -1102,6 +904,7 @@ impl CoreManager {
/// 重启内核
pub async fn restart_core(&self) -> Result<()> {
logging!(info, Type::Core, true, "Restarting core");
self.stop_core().await?;
self.start_core().await?;
Ok(())
@@ -1143,3 +946,6 @@ impl CoreManager {
Ok(())
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
+2 -2
View File
@@ -1,8 +1,8 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use tokio::time::{Duration, sleep, timeout};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery;
+20 -37
View File
@@ -2,8 +2,9 @@ use crate::singleton;
use parking_lot::RwLock;
use std::{
sync::{
Arc,
atomic::{AtomicU64, Ordering},
mpsc, Arc,
mpsc,
},
thread,
time::{Duration, Instant},
@@ -20,7 +21,6 @@ enum FrontendEvent {
NoticeMessage { status: String, message: String },
ProfileChanged { current_profile_id: String },
TimerUpdated { profile_index: String },
StartupCompleted,
ProfileUpdateStarted { uid: String },
ProfileUpdateCompleted { uid: String },
}
@@ -98,16 +98,14 @@ impl NotificationSystem {
let is_emergency = *system.emergency_mode.read();
if is_emergency {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
if is_emergency
&& let FrontendEvent::NoticeMessage { ref status, .. } = event
&& status == "info" {
log::warn!(
"Emergency mode active, skipping info message"
);
continue;
}
}
}
if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now();
@@ -134,9 +132,6 @@ impl NotificationSystem {
FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
}
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
}
@@ -203,13 +198,12 @@ impl NotificationSystem {
/// 发送事件到队列
fn send_event(&self, event: FrontendEvent) -> bool {
if *self.emergency_mode.read() {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
log::info!("Skipping info message in emergency mode");
return false;
}
}
if *self.emergency_mode.read()
&& let FrontendEvent::NoticeMessage { ref status, .. } = event
&& status == "info"
{
log::info!("Skipping info message in emergency mode");
return false;
}
if let Some(sender) = &self.sender {
@@ -366,22 +360,6 @@ impl Handle {
}
}
pub fn notify_startup_completed() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::StartupCompleted);
} else {
log::warn!(
"Notification system not initialized when trying to send StartupCompleted event."
);
}
}
pub fn notify_profile_update_started(uid: String) {
let handle = Self::global();
if handle.is_exiting() {
@@ -392,7 +370,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
} else {
log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event.");
log::warn!(
"Notification system not initialized when trying to send ProfileUpdateStarted event."
);
}
}
@@ -406,7 +386,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
} else {
log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event.");
log::warn!(
"Notification system not initialized when trying to send ProfileUpdateCompleted event."
);
}
}
@@ -471,8 +453,9 @@ impl Handle {
info,
Type::Frontend,
true,
"发送{}条启动时累积的错误消息",
errors.len()
"发送{}条启动时累积的错误消息: {:?}",
errors.len(),
errors
);
// 启动单独线程处理启动错误,避免阻塞主线程
+8 -8
View File
@@ -1,10 +1,10 @@
use crate::process::AsyncHandler;
use crate::utils::notification::{notify_event, NotificationEvent};
use crate::utils::notification::{NotificationEvent, notify_event};
use crate::{
config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use parking_lot::Mutex;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager};
@@ -109,7 +109,7 @@ impl Hotkey {
match function {
HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || {
crate::feat::open_or_close_dashboard_hotkey().await;
crate::feat::open_or_close_dashboard().await;
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
});
}
@@ -243,11 +243,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 window.is_focused().unwrap_or(false) {
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
}
if let Some(window) = app_handle_cloned.get_webview_window("main")
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
File diff suppressed because it is too large Load Diff
+186 -179
View File
@@ -1,11 +1,15 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use backoff::{Error as BackoffError, ExponentialBackoff};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::windows::named_pipe::ClientOptions;
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service"
@@ -112,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
Ok(expected_signature == response.signature)
}
// IPC连接管理-win
#[cfg(target_os = "windows")]
fn create_backoff_strategy() -> ExponentialBackoff {
ExponentialBackoff {
initial_interval: Duration::from_millis(50),
max_interval: Duration::from_secs(1),
max_elapsed_time: Some(Duration::from_secs(3)),
multiplier: 1.5,
..Default::default()
}
}
pub async fn send_ipc_request(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
use std::{
ffi::CString,
fs::File,
io::{Read, Write},
os::windows::io::{FromRawHandle, RawHandle},
ptr,
};
use winapi::um::{
fileapi::{CreateFileA, OPEN_EXISTING},
handleapi::INVALID_HANDLE_VALUE,
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
};
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
let operation = || async {
match send_ipc_request_internal(command.clone(), payload.clone()).await {
Ok(response) => Ok(response),
Err(e) => {
logging!(
warn,
Type::Service,
true,
"IPC请求失败,准备重试: 命令={}, 错误={}",
command_type,
e
);
Err(BackoffError::transient(e))
}
}
};
let request_json = serde_json::to_string(&request)?;
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> {
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
Ok(name) => name,
Err(e) => {
logging!(error, Type::Service, true, "创建CString失败: {}", e);
return Err(anyhow::anyhow!("创建CString失败: {}", e));
}
};
let handle = unsafe {
CreateFileA(
c_pipe_name.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let error = std::io::Error::last_os_error();
match backoff::future::retry(create_backoff_strategy(), operation).await {
Ok(response) => {
// logging!(
// info,
// Type::Service,
// true,
// "IPC请求成功: 命令={}, 成功={}",
// command_type,
// response.success
// );
Ok(response)
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"连接到服务命名管道失败: {}",
error
"IPC请求最终失败,重试已耗尽: 命令={}, 错误={}",
command_type,
e
);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
Err(anyhow::anyhow!("IPC请求重试失败: {}", e))
}
}
}
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
// 内部IPC请求实现(不带重试)
async fn send_ipc_request_internal(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
#[cfg(target_os = "windows")]
{
send_ipc_request_windows(command, payload).await
}
#[cfg(target_family = "unix")]
{
send_ipc_request_unix(command, payload).await
}
}
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
// IPC连接管理-win
#[cfg(target_os = "windows")]
async fn send_ipc_request_windows(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
let request = create_signed_request(command, payload)?;
let request_json = serde_json::to_string(&request)?;
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = pipe.write_all(&len_bytes) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) {
Ok(p) => p,
Err(e) => {
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
}
};
if let Err(e) = pipe.write_all(request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
if let Err(e) = pipe.flush() {
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
}
pipe.write_all(&len_bytes).await?;
pipe.write_all(request_bytes).await?;
pipe.flush().await?;
let mut response_len_bytes = [0u8; 4];
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
let mut response_len_bytes = [0u8; 4];
pipe.read_exact(&mut response_len_bytes).await?;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
pipe.read_exact(&mut response_bytes).await?;
let mut response_bytes = vec![0u8; response_len];
if let Err(e) = pipe.read_exact(&mut response_bytes) {
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
}
let response: IpcResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response)
})
.await??;
Ok(result)
Ok(response)
}
// IPC连接管理-unix
#[cfg(target_family = "unix")]
pub async fn send_ipc_request(
async fn send_ipc_request_unix(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
use std::os::unix::net::UnixStream;
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
}
};
let request = create_signed_request(command, payload)?;
let request_json = serde_json::to_string(&request)?;
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
Ok(s) => {
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
s
}
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await {
Ok(s) => s,
Err(e) => {
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
@@ -285,58 +253,97 @@ pub async fn send_ipc_request(
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
}
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
stream.write_all(&len_bytes).await?;
stream.write_all(request_bytes).await?;
stream.flush().await?;
// 读取响应长度
let mut response_len_bytes = [0u8; 4];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
stream.read_exact(&mut response_len_bytes).await?;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
stream.read_exact(&mut response_bytes).await?;
let response: IpcResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_signed_request() {
let command = IpcCommand::GetVersion;
let payload = serde_json::json!({"test": "data"});
let result = create_signed_request(command, payload);
assert!(result.is_ok());
if let Ok(request) = result {
assert!(!request.id.is_empty());
assert!(!request.signature.is_empty());
assert_eq!(request.command, IpcCommand::GetVersion);
}
}
#[test]
fn test_sign_and_verify_message() {
let test_message = "test message for signing";
let signature_result = sign_message(test_message);
assert!(signature_result.is_ok());
if let Ok(signature) = signature_result {
assert!(!signature.is_empty());
// 测试相同消息产生相同签名
if let Ok(signature2) = sign_message(test_message) {
assert_eq!(signature, signature2);
}
}
}
#[test]
fn test_verify_response_signature() {
let response = IpcResponse {
id: "test-id".to_string(),
success: true,
data: Some(serde_json::json!({"result": "success"})),
error: None,
signature: String::new(),
};
// 创建正确的签名
let verification_response = IpcResponse {
id: response.id.clone(),
success: response.success,
data: response.data.clone(),
error: response.error.clone(),
signature: String::new(),
};
if let Ok(message) = serde_json::to_string(&verification_response)
&& let Ok(correct_signature) = sign_message(&message)
{
let signed_response = IpcResponse {
signature: correct_signature,
..response
};
let verification_result = verify_response_signature(&signed_response);
assert!(verification_result.is_ok());
if let Ok(is_valid) = verification_result {
assert!(is_valid);
}
}
}
}
+2 -3
View File
@@ -2,7 +2,7 @@
use crate::utils::autostart as startup_shortcut;
use crate::{
config::{Config, IVerge},
core::{handle::Handle, EventDrivenProxyManager},
core::{EventDrivenProxyManager, handle::Handle},
logging, logging_error, singleton_lazy,
utils::logging::Type,
};
@@ -24,8 +24,7 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;
static DEFAULT_BYPASS: &str =
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
#[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
async fn get_bypass() -> String {
let use_default = Config::verge()
+15 -16
View File
@@ -6,8 +6,8 @@ use std::{
collections::HashMap,
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
@@ -242,19 +242,18 @@ impl Timer {
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
for item in items.iter() {
if let Some(option) = item.option.as_ref() {
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
if interval > 0 {
logging!(
debug,
Type::Timer,
"找到定时更新配置: uid={}, interval={}min",
uid,
interval
);
new_map.insert(uid.clone(), interval);
}
}
if let Some(option) = item.option.as_ref()
&& let (Some(interval), Some(uid)) = (option.update_interval, &item.uid)
&& interval > 0
{
logging!(
debug,
Type::Timer,
"找到定时更新配置: uid={}, interval={}min",
uid,
interval
);
new_map.insert(uid.clone(), interval);
}
}
}
@@ -390,7 +389,8 @@ impl Timer {
};
// Get the profile updated timestamp - now safe to await
let profiles = { Config::profiles().await.clone().data_ref() }.clone();
let config_profiles = Config::profiles().await;
let profiles = config_profiles.data_ref().clone();
let items = match profiles.get_items() {
Some(i) => i,
None => {
@@ -434,7 +434,6 @@ impl Timer {
/// Emit update events for frontend notification
fn emit_update_event(_uid: &str, _is_start: bool) {
#[cfg(any(feature = "verge-dev", feature = "default"))]
{
if _is_start {
super::handle::Handle::notify_profile_update_started(_uid.to_string());
+39 -53
View File
@@ -1,12 +1,14 @@
use once_cell::sync::OnceCell;
use tauri::tray::TrayIconBuilder;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::ipc::Rate;
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
use crate::{
cmd,
Type, cmd,
config::Config,
feat,
ipc::IpcManager,
@@ -14,7 +16,6 @@ use crate::{
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n::t},
Type,
};
use super::handle;
@@ -27,9 +28,9 @@ use std::{
time::{Duration, Instant},
};
use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconEvent},
AppHandle, Wry,
};
#[derive(Clone)]
@@ -74,12 +75,11 @@ impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon {
if let Ok(Some(common_icon_path)) = find_target_icons("common") {
if let Ok(icon_data) = fs::read(common_icon_path) {
return (true, icon_data);
}
}
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
&& let Ok(icon_data) = fs::read(common_icon_path)
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
@@ -109,12 +109,11 @@ impl TrayState {
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon {
if let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") {
if let Ok(icon_data) = fs::read(sysproxy_icon_path) {
return (true, icon_data);
}
}
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
&& let Ok(icon_data) = fs::read(sysproxy_icon_path)
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
@@ -144,12 +143,11 @@ impl TrayState {
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon {
if let Ok(Some(tun_icon_path)) = find_target_icons("tun") {
if let Ok(icon_data) = fs::read(tun_icon_path) {
return (true, icon_data);
}
}
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
&& let Ok(icon_data) = fs::read(tun_icon_path)
{
return (true, icon_data);
}
#[cfg(target_os = "macos")]
{
@@ -429,13 +427,13 @@ impl Tray {
{
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
if let Some(current_profile_uid) = profiles.get_current() {
if let Ok(profile) = profiles.get_item(&current_profile_uid) {
current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
}
if let Some(current_profile_uid) = profiles.get_current()
&& let Ok(profile) = profiles.get_item(&current_profile_uid)
{
current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
}
}
@@ -532,14 +530,9 @@ impl Tray {
feat::toggle_tun_mode(None).await;
}),
"main_window" => Box::pin(async move {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray点击事件: 显示主窗口");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
crate::module::lightweight::exit_lightweight_mode().await;
}
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "窗口显示结果: {result:?}");
if !lightweight::exit_lightweight_mode().await {
WindowManager::show_main_window().await;
};
}),
_ => Box::pin(async move {}),
};
@@ -642,7 +635,9 @@ async fn create_tray_menu(
let should_show = match mode {
"global" => group_name == "GLOBAL",
_ => group_name != "GLOBAL",
};
} &&
// Check if the group is hidden
!group_data.get("hidden").and_then(|v| v.as_bool( )).unwrap_or(false);
if !should_show {
continue;
@@ -971,19 +966,14 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into()).await; // Await async function
}
"open_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "Exiting Lightweight Mode");
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
}
let result = WindowManager::show_main_window().await; // Await async function
logging!(info, Type::Window, true, "Show Main Window: {result:?}");
if !lightweight::exit_lightweight_mode().await {
WindowManager::show_main_window().await;
};
}
"system_proxy" => {
feat::toggle_system_proxy().await; // Await async function
@@ -1008,14 +998,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
return;
}
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
if was_lightweight {
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window().await; // Await async function
logging!(info, Type::Window, true, "Show Main Window: {result:?}");
if !is_in_lightweight_mode() {
lightweight::entry_lightweight_mode().await; // Await async function
} else {
crate::module::lightweight::entry_lightweight_mode().await; // Remove .await as it's not async
lightweight::exit_lightweight_mode().await; // Await async function
}
}
"quit" => {
+1 -1
View File
@@ -1,7 +1,7 @@
#![cfg(target_os = "windows")]
use crate::utils::dirs;
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
+2 -4
View File
@@ -18,12 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
deep_merge(&mut config, &Value::from(merge));
let config = config.as_mapping().cloned().unwrap_or_else(|| {
config.as_mapping().cloned().unwrap_or_else(|| {
log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new()
});
config
})
}
#[test]
+18 -21
View File
@@ -384,29 +384,26 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists() {
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
if let Ok(dns_config) =
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
// 处理hosts配置
if let Some(hosts_value) = dns_config.get("hosts") {
if hosts_value.is_mapping() {
config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration");
}
}
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)
{
// 处理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");
}
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());
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");
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ pub fn use_script(
config: Mapping,
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsString, JsValue, Source};
use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction};
use std::{cell::RefCell, rc::Rc};
let mut context = Context::default();
+30 -30
View File
@@ -44,39 +44,39 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
// If this is proxies field, we also need to filter proxy-groups
if field == "proxies" {
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") {
let mut new_groups = Sequence::new();
for group in groups {
if let Value::Mapping(group_map) = group {
let mut new_group = group_map.clone();
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
let filtered_proxies: Sequence = proxies
.iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.contains(name)
} else {
true
}
})
.cloned()
.collect();
new_group.insert(
Value::String("proxies".into()),
Value::Sequence(filtered_proxies),
);
}
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
if field == "proxies"
&& let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups")
{
let mut new_groups = Sequence::new();
for group in groups {
if let Value::Mapping(group_map) = group {
let mut new_group = group_map.clone();
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
let filtered_proxies: Sequence = proxies
.iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.contains(name)
} else {
true
}
})
.cloned()
.collect();
new_group.insert(
Value::String("proxies".into()),
Value::Sequence(filtered_proxies),
);
}
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
}
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
}
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
}
config
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::{
config::Config,
core::{handle, tray, CoreManager},
core::{CoreManager, handle, tray},
ipc::IpcManager,
logging_error,
process::AsyncHandler,
+5 -5
View File
@@ -1,6 +1,6 @@
use crate::{
config::{Config, IVerge},
core::{handle, hotkey, sysopt, tray, CoreManager},
core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error,
module::lightweight,
utils::logging::Type,
@@ -202,10 +202,10 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys).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?;
+3 -3
View File
@@ -1,11 +1,11 @@
use crate::{
cmd,
config::{profiles::profiles_draft_update_item_safe, Config, PrfItem, PrfOption},
core::{handle, tray, CoreManager},
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{CoreManager, handle, tray},
logging,
utils::logging::Type,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
+9 -8
View File
@@ -13,21 +13,22 @@ pub async fn toggle_system_proxy() {
// 获取当前系统代理状态
let enable = {
let verge = Config::verge().await;
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
enable
verge.latest_ref().enable_system_proxy.unwrap_or(false)
};
// 获取自动关闭连接设置
let auto_close_connection = {
let verge = Config::verge().await;
let auto_close = verge.latest_ref().auto_close_connection.unwrap_or(false);
auto_close
verge.latest_ref().auto_close_connection.unwrap_or(false)
};
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable && auto_close_connection {
if let Err(err) = IpcManager::global().close_all_connections().await {
log::error!(target: "app", "Failed to close all connections: {err}");
}
if enable
&& auto_close_connection
&& let Err(err) = IpcManager::global().close_all_connections().await
{
log::error!(target: "app", "Failed to close all connections: {err}");
}
let patch_result = super::patch_verge(
+11 -47
View File
@@ -1,57 +1,21 @@
use crate::utils::window_manager::WindowManager;
use crate::{
config::Config,
core::{handle, sysopt, CoreManager},
core::{CoreManager, handle, sysopt},
ipc::IpcManager,
logging,
module::lightweight,
utils::logging::Type,
};
/// Open or close the dashboard window
#[allow(dead_code)]
pub async fn open_or_close_dashboard() {
open_or_close_dashboard_internal(false).await
}
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
#[allow(dead_code)]
pub async fn open_or_close_dashboard_hotkey() {
open_or_close_dashboard_internal(true).await
open_or_close_dashboard_internal().await
}
/// Internal implementation for opening/closing dashboard
async fn open_or_close_dashboard_internal(bypass_debounce: bool) {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
// 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
log::info!(target: "app", "主线程中执行热键窗口操作");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode().await;
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Window toggle result: {result:?}");
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode().await;
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
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:?}");
}
@@ -92,7 +56,7 @@ pub async fn quit() {
}
async fn clean_async() -> bool {
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
logging!(info, Type::System, true, "开始执行异步清理操作...");
@@ -252,10 +216,10 @@ pub async fn hide() {
add_light_weight_timer().await;
}
if let Some(window) = handle::Handle::global().get_window() {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
}
if let Some(window) = handle::Handle::global().get_window()
&& window.is_visible().unwrap_or(false)
{
let _ = window.hide();
}
handle::Handle::global().set_activation_policy_accessory();
}
+9 -11
View File
@@ -1,10 +1,10 @@
use std::time::Duration;
use kode_bridge::{
errors::{AnyError, AnyResult},
ClientConfig, IpcHttpClient, LegacyResponse,
errors::{AnyError, AnyResult},
};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use crate::{
logging, singleton_with_logging,
@@ -30,7 +30,8 @@ pub struct IpcManager {
}
impl IpcManager {
fn new() -> Self {
pub fn new() -> Self {
logging!(info, Type::Ipc, true, "Creating new IpcManager instance");
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
@@ -51,9 +52,6 @@ impl IpcManager {
}
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
impl IpcManager {
pub async fn request(
&self,
@@ -199,8 +197,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
self.send_request("GET", &url, None).await
}
// 版本和配置相关
@@ -279,7 +276,6 @@ impl IpcManager {
let payload = serde_json::json!({
"name": proxy
});
match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -340,8 +336,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
self.send_request("GET", &url, None).await
}
// 调试相关
@@ -370,3 +365,6 @@ impl IpcManager {
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
+24 -19
View File
@@ -1,6 +1,7 @@
#![allow(non_snake_case)]
#![recursion_limit = "512"]
mod cache;
mod cmd;
pub mod config;
mod core;
@@ -9,7 +10,6 @@ mod feat;
mod ipc;
mod module;
mod process;
mod state;
mod utils;
#[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager;
@@ -26,7 +26,7 @@ use tauri::Manager;
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
use utils::logging::Type;
/// Application initialization helper functions
@@ -74,9 +74,12 @@ mod app_init {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init());
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_http::init());
#[cfg(all(debug_assertions, not(feature = "tokio-trace")))]
// Devtools plugin only in debug mode with feature tauri-dev
// to avoid duplicated registering of logger since the devtools plugin also registers a logger
#[cfg(all(debug_assertions, not(feature = "tokio-trace"), feature = "tauri-dev"))]
{
builder = builder.plugin(tauri_plugin_devtools::init());
}
@@ -134,8 +137,8 @@ mod app_init {
}
/// Generate all command handlers for the application
pub fn generate_handlers(
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
pub fn generate_handlers()
-> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![
// Common commands
cmd::get_sys_proxy,
@@ -155,7 +158,6 @@ mod app_init {
// Application lifecycle
cmd::notify_ui_ready,
cmd::update_ui_stage,
cmd::reset_ui_ready_state,
cmd::get_running_mode,
cmd::get_app_uptime,
cmd::get_auto_launch_status,
@@ -178,6 +180,8 @@ mod app_init {
cmd::get_runtime_yaml,
cmd::get_runtime_exists,
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::get_proxies,
@@ -269,13 +273,14 @@ pub fn run() {
// Setup singleton check
app_init::init_singleton_check();
// We don't need init_portable_flag here anymore due to the init_config will do the things
// let _ = utils::dirs::init_portable_flag();
let _ = utils::dirs::init_portable_flag();
// Set Linux environment variable
#[cfg(target_os = "linux")]
{
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
@@ -284,7 +289,9 @@ pub fn run() {
let is_plasma_desktop = desktop_env.contains("PLASMA");
if is_kde_desktop || is_plasma_desktop {
std::env::set_var("GTK_CSD", "0");
unsafe {
std::env::set_var("GTK_CSD", "0");
}
logging!(
info,
Type::Setup,
@@ -438,10 +445,10 @@ pub fn run() {
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().init().await {
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
if !is_enable_global_hotkey
&& let Err(e) = hotkey::Hotkey::global().init().await
{
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
return;
}
@@ -474,10 +481,8 @@ pub fn run() {
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
}
if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
}
});
}
+133 -101
View File
@@ -1,38 +1,89 @@
use crate::{
cache::CacheProxy,
config::Config,
core::{handle, timer::Timer, tray::Tray},
log_err, logging,
process::AsyncHandler,
state::proxy::ProxyRequestCache,
utils::{logging::Type, window_manager::WindowManager},
utils::logging::Type,
};
#[cfg(target_os = "macos")]
use crate::logging_error;
use crate::utils::window_manager::WindowManager;
use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Listener, Manager};
use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
use tauri::Listener;
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
static IS_IN_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
fn inner_set_lightweight_mode(value: bool) -> bool {
if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
IS_IN_LIGHTWEIGHT.store(value, Ordering::SeqCst);
value
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LightweightState {
Normal = 0,
In = 1,
Exiting = 2,
}
fn inner_get_lightweight_mode() -> bool {
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst) || !WindowManager::is_main_window_exists()
impl From<u8> for LightweightState {
fn from(v: u8) -> Self {
match v {
1 => LightweightState::In,
2 => LightweightState::Exiting,
_ => LightweightState::Normal,
}
}
}
impl LightweightState {
fn as_u8(self) -> u8 {
self as u8
}
}
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, true, "轻量模式已关闭");
}
LightweightState::In => {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
}
LightweightState::Exiting => {
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
}
}
}
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
// 检查是否处于轻量模式
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() {
@@ -56,18 +107,8 @@ pub async fn run_once_auto_lightweight() {
return;
}
logging!(
info,
Type::Lightweight,
true,
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
set_lightweight_mode(true).await;
enable_auto_light_weight_mode().await;
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
pub async fn auto_lightweight_mode_init() -> Result<()> {
@@ -88,29 +129,13 @@ pub async fn auto_lightweight_mode_init() -> Result<()> {
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_lightweight_mode(true).await;
set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await;
}
Ok(())
}
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst)
}
// 设置轻量模式状态
pub async fn set_lightweight_mode(value: bool) {
if inner_get_lightweight_mode() != value {
inner_set_lightweight_mode(value);
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
}
pub async fn enable_auto_light_weight_mode() {
if let Err(e) = Timer::global().init().await {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
@@ -125,73 +150,67 @@ pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
let _ = cancel_light_weight_timer();
cancel_window_close_listener();
cancel_webview_focus_listener();
}
pub async fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::hide_main_window();
logging!(
info,
Type::Lightweight,
true,
"轻量模式隐藏窗口结果: {:?}",
result
);
if let Some(window) = handle::Handle::global().get_window() {
if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy();
}
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_accessory();
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()
{
logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用");
return false;
}
WindowManager::destroy_main_window();
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer();
ProxyRequestCache::global().clean_default_keys();
// 更新托盘显示
logging_error!(Type::Lightweight, true, Tray::global().update_part().await);
// 回到 In
set_state(LightweightState::In);
CacheProxy::global().clean_default_keys();
true
}
// 添加从轻量模式恢复的函数
pub async fn exit_lightweight_mode() {
// 使用原子操作检查是否已经在退出过程中,防止并发调用
if EXITING_LIGHTWEIGHT
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
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()
{
logging!(
info,
Type::Lightweight,
true,
"轻量模式退出操作已在进行中,跳过重复调用"
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
return;
return false;
}
// 使用defer确保无论如何都会重置标志
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
return;
}
WindowManager::show_main_window().await;
set_lightweight_mode(false).await;
let _ = cancel_light_weight_timer();
// macOS激活策略
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
// 回到 Normal
set_state(LightweightState::Normal);
// 重置UI就绪状态
crate::utils::resolve::ui::reset_ui_ready();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
logging!(info, Type::Lightweight, true, "轻量模式退出完成");
true
}
#[cfg(target_os = "macos")]
@@ -199,7 +218,7 @@ pub async fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer().await);
}
fn setup_window_close_listener() -> u32 {
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
@@ -214,12 +233,22 @@ fn setup_window_close_listener() -> u32 {
"监听到关闭请求,开始轻量模式计时"
);
});
return handler;
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
}
0
}
fn setup_webview_focus_listener() -> u32 {
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
}
}
}
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
@@ -229,15 +258,18 @@ fn setup_webview_focus_listener() -> u32 {
"监听到窗口获得焦点,取消轻量模式计时"
);
});
return handler;
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
}
0
}
fn cancel_window_close_listener() {
fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() {
window.unlisten(setup_window_close_listener());
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口焦点监听");
}
}
}
+8 -2
View File
@@ -1,6 +1,6 @@
use crate::{
cmd::system,
core::{handle, CoreManager},
core::{CoreManager, handle},
};
use std::fmt::{self, Debug, Formatter};
use sysinfo::System;
@@ -20,7 +20,13 @@ impl Debug for PlatformSpecification {
write!(
f,
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin
self.system_name,
self.system_version,
self.system_kernel_version,
self.system_arch,
self.verge_version,
self.running_mode,
self.is_admin
)
}
}
+9 -3
View File
@@ -67,12 +67,18 @@ impl AsyncHandler {
location.column()
);
println!("┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐");
println!(
"┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐"
);
println!("│ {:<18} │ {:<80} │", "Field", "Value");
println!("├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤");
println!(
"├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤"
);
println!("│ {:<18} │ {:<80} │", "Type of task", type_str);
println!("│ {:<18} │ {:<80} │", "Size of task", size_str);
println!("│ {:<18} │ {:<80} │", "Called from", loc_str);
println!("└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘");
println!(
"└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘"
);
}
}
-4
View File
@@ -1,4 +0,0 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod proxy;
+1 -1
View File
@@ -1,5 +1,5 @@
#[cfg(target_os = "windows")]
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
#[cfg(target_os = "windows")]
use log::info;
+5 -6
View File
@@ -151,12 +151,11 @@ pub fn find_target_icons(target: &str) -> Result<Option<String>> {
let entry = entry?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{
matching_files.push(path);
}
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{
matching_files.push(path);
}
}
+3 -7
View File
@@ -1,7 +1,7 @@
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize};
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::Mapping;
use std::{path::PathBuf, str::FromStr};
@@ -154,10 +154,6 @@ macro_rules! ret_err {
#[macro_export]
macro_rules! t {
($en:expr, $zh:expr, $use_zh:expr) => {
if $use_zh {
$zh
} else {
$en
}
if $use_zh { $zh } else { $en }
};
}
+16 -17
View File
@@ -15,14 +15,14 @@ fn get_locales_dir() -> Option<PathBuf> {
pub fn get_supported_languages() -> Vec<String> {
let mut languages = Vec::new();
if let Some(locales_dir) = get_locales_dir() {
if let Ok(entries) = fs::read_dir(locales_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str() {
if let Some(lang) = file_name.strip_suffix(".json") {
languages.push(lang.to_string());
}
}
if let Some(locales_dir) = get_locales_dir()
&& let Ok(entries) = fs::read_dir(locales_dir)
{
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str()
&& let Some(lang) = file_name.strip_suffix(".json")
{
languages.push(lang.to_string());
}
}
}
@@ -39,10 +39,10 @@ static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{lang}.json"));
if let Ok(content) = fs::read_to_string(file_path) {
if let Ok(json) = serde_json::from_str(&content) {
translations.insert(lang.to_string(), json);
}
if let Ok(content) = fs::read_to_string(file_path)
&& let Ok(json) = serde_json::from_str(&content)
{
translations.insert(lang.to_string(), json);
}
}
}
@@ -76,14 +76,13 @@ pub async fn t(key: &str) -> String {
return text.to_string();
}
if current_lang != DEFAULT_LANGUAGE {
if let Some(text) = TRANSLATIONS
if current_lang != DEFAULT_LANGUAGE
&& let Some(text) = TRANSLATIONS
.get(DEFAULT_LANGUAGE)
.and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str())
{
return text.to_string();
}
{
return text.to_string();
}
key
+82 -89
View File
@@ -1,3 +1,11 @@
cfg_if::cfg_if! {
if #[cfg(not(feature = "tauri-dev"))] {
use crate::utils::logging::{console_colored_format, file_format};
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, LogSpecification, Logger};
use log::LevelFilter;
}
}
use crate::{
config::*,
core::handle,
@@ -7,64 +15,47 @@ use crate::{
};
use anyhow::Result;
use chrono::{Local, TimeZone};
use log::LevelFilter;
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Logger, Root},
encode::pattern::PatternEncoder,
};
use std::{path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
use tokio::fs::DirEntry;
/// initialize this instance's log file
async fn init_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
if !log_dir.exists() {
let _ = tokio::fs::create_dir_all(&log_dir).await;
}
#[cfg(not(feature = "tauri-dev"))]
pub async fn init_logger() -> Result<()> {
let log_level = Config::verge().await.latest_ref().get_log_level();
if log_level == LevelFilter::Off {
return Ok(());
return Err(anyhow::anyhow!("logging is disabled"));
}
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{local_time}.log");
let log_file = log_dir.join(log_file);
let log_dir = dirs::app_logs_dir()?;
let logger = Logger::with(LogSpecification::from(log_level))
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(Duplicate::Debug)
.format(console_colored_format)
.format_for_files(file_format)
.rotate(
// ? 总是保持单个日志最大 10 MB
Criterion::Size(10 * 1024 * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
// TODO 提供前端设置最大保留文件数量
Cleanup::Never,
);
let log_pattern = match log_level {
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
};
let _handle = logger.start()?;
let encode = Box::new(PatternEncoder::new(log_pattern));
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
let mut logger_builder = Logger::builder();
let mut root_builder = Root::builder();
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
logger_builder = logger_builder.appenders(["file"]);
if log_more {
root_builder = root_builder.appenders(["file"]);
}
let (config, _) = log4rs::config::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile)))
.logger(logger_builder.additive(false).build("app", log_level))
.build_lossy(root_builder.build(log_level));
log4rs::init_config(config)?;
// TODO 全局 logger handle 控制
// GlobalLoggerProxy::global().set_inner(handle);
// TODO 提供前端设置等级,热更新等级
// logger.parse_new_spec(spec)
Ok(())
}
// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件
/// 删除log文件
pub async fn delete_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
@@ -296,52 +287,52 @@ async fn ensure_directories() -> Result<()> {
/// 初始化配置文件
async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::clash_path() {
if !path.exists() {
let template = IClashTemp::template().0;
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created clash config at {:?}",
path
);
}
if let Ok(path) = dirs::clash_path()
&& !path.exists()
{
let template = IClashTemp::template().0;
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created clash config at {:?}",
path
);
}
if let Ok(path) = dirs::verge_path() {
if !path.exists() {
let template = IVerge::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created verge config at {:?}",
path
);
}
if let Ok(path) = dirs::verge_path()
&& !path.exists()
{
let template = IVerge::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created verge config at {:?}",
path
);
}
if let Ok(path) = dirs::profiles_path() {
if !path.exists() {
let template = IProfiles::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created profiles config at {:?}",
path
);
}
if let Ok(path) = dirs::profiles_path()
&& !path.exists()
{
let template = IProfiles::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created profiles config at {:?}",
path
);
}
// 验证并修正verge配置
@@ -355,11 +346,13 @@ async fn initialize_config_files() -> Result<()> {
/// Initialize all the config files
/// before tauri setup
pub async fn init_config() -> Result<()> {
let _ = dirs::init_portable_flag();
// We do not need init_portable_flag here anymore due to lib.rs will to the things
// let _ = dirs::init_portable_flag();
if let Err(e) = init_log().await {
eprintln!("Failed to initialize logging: {}", e);
}
// We do not need init_log here anymore due to resolve will to the things
// if let Err(e) = init_log().await {
// eprintln!("Failed to initialize logging: {}", e);
// }
ensure_directories().await?;
@@ -459,7 +452,7 @@ pub async fn init_resources() -> Result<()> {
#[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> {
use tauri::utils::platform::current_exe;
use winreg::{enums::*, RegKey};
use winreg::{RegKey, enums::*};
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
+66 -2
View File
@@ -1,4 +1,14 @@
use std::fmt;
cfg_if::cfg_if! {
if #[cfg(feature = "tauri-dev")] {
use std::fmt;
} else {
#[cfg(feature = "verge-dev")]
use nu_ansi_term::Color;
use std::{fmt, io::Write, thread};
use flexi_logger::DeferredNow;
use log::{LevelFilter, Record};
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
@@ -107,7 +117,8 @@ macro_rules! wrap_err {
macro_rules! logging {
// 带 println 的版本(支持格式化参数)
($level:ident, $type:expr, true, $($arg:tt)*) => {
println!("{} {}", $type, format_args!($($arg)*));
// We dont need println here anymore
// println!("{} {}", $type, format_args!($($arg)*));
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
@@ -157,3 +168,56 @@ macro_rules! logging_error {
logging_error!($type, false, $fmt $(, $arg)*);
};
}
#[cfg(not(feature = "tauri-dev"))]
pub fn get_log_level(log_level: &LevelFilter) -> String {
#[cfg(feature = "verge-dev")]
match log_level {
LevelFilter::Off => Color::Fixed(8).paint("OFF").to_string(),
LevelFilter::Error => Color::Red.paint("ERROR").to_string(),
LevelFilter::Warn => Color::Yellow.paint("WARN ").to_string(),
LevelFilter::Info => Color::Green.paint("INFO ").to_string(),
LevelFilter::Debug => Color::Blue.paint("DEBUG").to_string(),
LevelFilter::Trace => Color::Purple.paint("TRACE").to_string(),
}
#[cfg(not(feature = "verge-dev"))]
log_level.to_string()
}
#[cfg(not(feature = "tauri-dev"))]
pub fn console_colored_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &log::Record,
) -> std::io::Result<()> {
let current_thread = thread::current();
let thread_name = current_thread.name().unwrap_or("unnamed");
let level = get_log_level(&record.level().to_level_filter());
let line = record.line().unwrap_or(0);
write!(
w,
"[{}] {} [{}:{}] T[{}] {}",
now.format("%H:%M:%S%.3f"),
level,
record.module_path().unwrap_or("<unnamed>"),
line,
thread_name,
record.args(),
)
}
#[cfg(not(feature = "tauri-dev"))]
pub fn file_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &Record,
) -> std::io::Result<()> {
write!(
w,
"[{}] {} {}",
now.format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
record.args(),
)
}
+20 -20
View File
@@ -1,14 +1,14 @@
use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use base64::{Engine as _, engine::general_purpose};
use isahc::prelude::*;
use isahc::{HttpClient, config::SslOption};
use isahc::{
config::RedirectPolicy,
http::{
header::{HeaderMap, HeaderValue, USER_AGENT},
StatusCode, Uri,
header::{HeaderMap, HeaderValue, USER_AGENT},
},
};
use isahc::{config::SslOption, HttpClient};
use std::time::{Duration, Instant};
use sysproxy::Sysproxy;
use tauri::Url;
@@ -88,10 +88,11 @@ impl NetworkManager {
return true;
}
if let Some((time, _)) = &*last_error_guard {
if time.elapsed() < Duration::from_secs(30) && count > 2 {
return true;
}
if let Some((time, _)) = &*last_error_guard
&& time.elapsed() < Duration::from_secs(30)
&& count > 2
{
return true;
}
false
@@ -113,7 +114,8 @@ impl NetworkManager {
) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone();
let client = {
{
let mut builder = HttpClient::builder();
builder = match proxy_uri_clone {
@@ -136,9 +138,7 @@ impl NetworkManager {
builder = builder.redirect_policy(RedirectPolicy::Follow);
Ok(builder.build()?)
};
client
}
}
pub async fn create_request(
@@ -200,15 +200,15 @@ impl NetworkManager {
let parsed = Url::parse(url)?;
let mut extra_headers = HeaderMap::new();
if !parsed.username().is_empty() {
if let Some(pass) = parsed.password() {
let auth_str = format!("{}:{}", parsed.username(), pass);
let encoded = general_purpose::STANDARD.encode(auth_str);
extra_headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded))?,
);
}
if !parsed.username().is_empty()
&& let Some(pass) = parsed.password()
{
let auth_str = format!("{}:{}", parsed.username(), pass);
let encoded = general_purpose::STANDARD.encode(auth_str);
extra_headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded))?,
);
}
let clean_url = {
+37 -4
View File
@@ -3,11 +3,13 @@ use tauri::AppHandle;
use crate::{
config::Config,
core::{handle, hotkey::Hotkey, sysopt, tray::Tray, CoreManager, Timer},
core::{
CoreManager, Timer, handle, hotkey::Hotkey, service::SERVICE_MANAGER, sysopt, tray::Tray,
},
logging, logging_error,
module::lightweight::auto_lightweight_mode_init,
module::lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
process::AsyncHandler,
utils::{init, logging::Type, resolve::window::create_window, server},
utils::{init, logging::Type, server, window_manager::WindowManager},
};
pub mod dns;
@@ -38,6 +40,10 @@ pub fn resolve_setup_async() {
);
AsyncHandler::spawn(|| async {
#[cfg(not(feature = "tauri-dev"))]
resolve_setup_logger().await;
init_service_manager().await;
futures::join!(
init_work_config(),
init_resources(),
@@ -46,6 +52,7 @@ pub fn resolve_setup_async() {
);
init_timer().await;
init_once_auto_lightweight().await;
init_auto_lightweight_mode().await;
init_verge_config().await;
@@ -114,6 +121,12 @@ pub(super) fn init_scheme() {
logging_error!(Type::Setup, true, init::init_scheme());
}
#[cfg(not(feature = "tauri-dev"))]
pub(super) async fn resolve_setup_logger() {
logging!(info, Type::Setup, true, "Initializing global logger...");
logging_error!(Type::Setup, true, init::init_logger().await);
}
pub async fn resolve_scheme(param: String) -> Result<()> {
logging!(
info,
@@ -150,6 +163,16 @@ pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, true, Hotkey::global().init().await);
}
pub(super) async fn init_once_auto_lightweight() {
logging!(
info,
Type::Lightweight,
true,
"Running auto lightweight mode check..."
);
run_once_auto_lightweight().await;
}
pub(super) async fn init_auto_lightweight_mode() {
logging!(
info,
@@ -185,6 +208,15 @@ pub(super) async fn init_verge_config() {
logging_error!(Type::Setup, true, Config::init_config().await);
}
pub(super) async fn init_service_manager() {
logging!(info, Type::Setup, true, "Initializing service manager...");
logging_error!(
Type::Setup,
true,
SERVICE_MANAGER.lock().await.refresh().await
);
}
pub(super) async fn init_core_manager() {
logging!(info, Type::Setup, true, "Initializing core manager...");
logging_error!(Type::Setup, true, CoreManager::global().init().await);
@@ -219,6 +251,7 @@ pub(super) async fn refresh_tray_menu() {
}
pub(super) async fn init_window() {
logging!(info, Type::Setup, true, "Initializing main window...");
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
@@ -229,5 +262,5 @@ pub(super) async fn init_window() {
Handle::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start).await;
WindowManager::create_window(!is_silent_start).await;
}
+1 -2
View File
@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use percent_encoding::percent_decode_str;
use tauri::Url;
@@ -44,7 +44,6 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
// create_window(false).await;
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = match item.uid.clone() {
+1 -34
View File
@@ -1,8 +1,8 @@
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::Notify;
@@ -51,26 +51,6 @@ fn get_ui_ready_notify() -> &'static Arc<Notify> {
UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new()))
}
/// 等待 UI 就绪的异步函数,使用事件驱动而非轮询
pub async fn wait_for_ui_ready(timeout_seconds: u64) -> bool {
// 首先检查是否已经就绪
if get_ui_ready().load(Ordering::Acquire) {
return true;
}
// 使用 tokio::select! 同时等待通知和超时
tokio::select! {
_ = get_ui_ready_notify().notified() => {
// 收到通知后再次确认状态(防止虚假通知)
get_ui_ready().load(Ordering::Acquire)
}
_ = tokio::time::sleep(std::time::Duration::from_secs(timeout_seconds)) => {
// 超时,返回当前状态
get_ui_ready().load(Ordering::Acquire)
}
}
}
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
@@ -91,16 +71,3 @@ pub fn mark_ui_ready() {
// 通知所有等待的任务
get_ui_ready_notify().notify_waiters();
}
// 重置UI就绪状态
pub fn reset_ui_ready() {
get_ui_ready().store(false, Ordering::Release);
{
let state = get_ui_ready_state();
let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted;
}
logging!(info, Type::Window, true, "UI就绪状态已重置");
// 注意:这里不需要通知,因为重置后状态变为 false
}
+10 -256
View File
@@ -1,20 +1,11 @@
use std::time::{Duration, Instant};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use tauri::{Manager, WebviewWindow};
use tauri::WebviewWindow;
use crate::{
core::handle,
logging,
module::lightweight,
process::AsyncHandler,
logging, logging_error,
utils::{
logging::Type,
resolve::{
ui::{get_ui_ready, update_ui_ready_stage, wait_for_ui_ready, UiReadyStage},
window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
},
resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
},
};
@@ -25,68 +16,8 @@ const DEFAULT_HEIGHT: f64 = 700.0;
const MINIMAL_WIDTH: f64 = 520.0;
const MINIMAL_HEIGHT: f64 = 520.0;
// 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
/// 检查是否已存在窗口,如果存在则显示并返回 true
fn check_existing_window(is_show: bool) -> Option<bool> {
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
}
return Some(true);
}
}
None
}
/// 获取窗口创建锁,防止并发创建
fn acquire_window_creation_lock() -> Result<(), bool> {
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
let (is_creating, last_time) = *creating;
let elapsed = last_time.elapsed();
if is_creating && elapsed < Duration::from_secs(2) {
logging!(
info,
Type::Window,
true,
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis()
);
return Err(false);
}
*creating = (true, Instant::now());
Ok(())
}
/// 重置窗口创建锁
fn reset_window_creation_lock() {
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
*creating = (false, Instant::now());
logging!(debug, Type::Window, true, "窗口创建状态已重置");
}
/// 构建新的 WebView 窗口
fn build_new_window() -> Result<WebviewWindow, String> {
pub fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
logging!(
error,
@@ -97,7 +28,7 @@ fn build_new_window() -> Result<WebviewWindow, String> {
"无法获取app_handle".to_string()
})?;
tauri::WebviewWindowBuilder::new(
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
@@ -111,188 +42,11 @@ fn build_new_window() -> Result<WebviewWindow, String> {
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(WINDOW_INITIAL_SCRIPT)
.build()
.map_err(|e| {
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
e.to_string()
})
}
/// 窗口创建后的初始设置
async fn setup_window_post_creation() {
update_ui_ready_stage(UiReadyStage::NotStarted);
handle::Handle::global().mark_startup_completed();
logging!(
debug,
Type::Window,
true,
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight().await;
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
}
/// 通过窗口标签处理窗口显示逻辑(减少任务大小的优化版本)
fn handle_window_display_by_label(window_label: String, is_show: bool) {
if !is_show {
logging!(
debug,
Type::Window,
true,
"is_show为false,窗口保持隐藏状态"
);
return;
}
// 通过标签重新获取窗口实例
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(error, Type::Window, true, "无法获取app handle");
return;
}
};
let window = match app_handle.get_webview_window(&window_label) {
Some(window) => window,
None => {
logging!(
error,
Type::Window,
true,
"无法通过标签获取窗口: {}",
window_label
);
return;
}
};
// 立即显示窗口
let _ = window.show();
let _ = window.set_focus();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_regular();
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
3
} else {
8
};
logging!(
info,
Type::Window,
true,
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
// 异步监控UI状态
AsyncHandler::spawn(move || async move {
monitor_ui_loading(timeout_seconds).await;
});
logging!(info, Type::Window, true, "窗口显示流程完成");
}
/// 监控 UI 加载状态 - 优雅的事件驱动版本
async fn monitor_ui_loading(timeout_seconds: u64) {
logging!(
debug,
Type::Window,
true,
"启动UI状态监控,使用事件驱动方式,超时{}秒",
timeout_seconds
);
// 使用事件驱动的方式等待 UI 就绪,完全消除轮询
let ui_ready = wait_for_ui_ready(timeout_seconds).await;
if ui_ready {
logging!(info, Type::Window, true, "UI已完全加载就绪(事件驱动)");
handle::Handle::global()
.get_window()
.map(|window| window.eval(INITIAL_LOADING_OVERLAY));
} else {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
// 超时后手动设置 UI 就绪状态(保持向后兼容性)
get_ui_ready().store(true, std::sync::atomic::Ordering::Release);
logging!(info, Type::Window, true, "超时后成功设置UI就绪状态");
}
}
pub async fn create_window(is_show: bool) -> bool {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
if !is_show {
lightweight::set_lightweight_mode(true).await;
handle::Handle::notify_startup_completed();
return false;
}
// 检查是否已存在窗口
if let Some(result) = check_existing_window(is_show) {
return result;
}
// 检查 app_handle 是否存在
if handle::Handle::global().app_handle().is_none() {
logging!(error, Type::Window, true, "No window found in app_handle");
return false;
}
// 获取窗口创建锁
if let Err(should_return) = acquire_window_creation_lock() {
return should_return;
}
// 构建新窗口
let newly_created_window = match build_new_window() {
{
Ok(window) => {
// 窗口创建成功,重置锁状态
reset_window_creation_lock();
window
logging_error!(Type::Window, true, window.eval(INITIAL_LOADING_OVERLAY));
Ok(window)
}
Err(_) => {
// 窗口创建失败,重置锁状态
reset_window_creation_lock();
return false;
}
};
logging!(debug, Type::Window, true, "主窗口实例创建成功");
// 获取窗口标签,减少闭包捕获的大小
let window_label = newly_created_window.label().to_string();
// 异步处理窗口后续设置,只捕获必要的小数据
setup_window_post_creation().await;
handle_window_display_by_label(window_label, is_show);
true
Err(e) => Err(e.to_string()),
}
}
+2 -2
View File
@@ -1,11 +1,11 @@
use super::resolve;
use crate::{
config::{Config, IVerge, DEFAULT_PAC},
config::{Config, DEFAULT_PAC, IVerge},
logging_error,
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use port_scanner::local_port_available;
use warp::Filter;
+58 -33
View File
@@ -1,4 +1,10 @@
use crate::{core::handle, logging, utils::logging::Type};
use crate::{
core::handle,
logging,
utils::{logging::Type, resolve::window::build_new_window},
};
use std::future::Future;
use std::pin::Pin;
use tauri::{Manager, WebviewWindow, Wry};
use once_cell::sync::OnceCell;
@@ -18,6 +24,8 @@ pub enum WindowOperationResult {
Hidden,
/// 创建了新窗口
Created,
/// 摧毁了窗口
Destroyed,
/// 操作失败
Failed,
/// 无需操作
@@ -138,7 +146,7 @@ impl WindowManager {
match current_state {
WindowState::NotExist => {
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window().await {
if Self::create_window(true).await {
logging!(info, Type::Window, true, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created
@@ -198,7 +206,7 @@ impl WindowManager {
// 窗口不存在,创建新窗口
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
// 由于已经有防抖保护,直接调用内部方法
if Self::create_new_window().await {
if Self::create_window(true).await {
WindowOperationResult::Created
} else {
WindowOperationResult::Failed
@@ -316,33 +324,6 @@ impl WindowManager {
}
}
/// 隐藏主窗口
pub fn hide_main_window() -> WindowOperationResult {
logging!(info, Type::Window, true, "开始隐藏主窗口");
match Self::get_main_window() {
Some(window) => match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "窗口已隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
},
None => {
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
WindowOperationResult::NoAction
}
}
}
/// 检查窗口是否存在
pub fn is_main_window_exists() -> bool {
Self::get_main_window().is_some()
}
/// 检查窗口是否可见
pub fn is_main_window_visible() -> bool {
Self::get_main_window()
@@ -365,10 +346,54 @@ impl WindowManager {
}
/// 创建新窗口,防抖避免重复调用
async fn create_new_window() -> bool {
use crate::utils::resolve;
pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
Box::pin(async move {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
resolve::window::create_window(true).await
if !is_show {
return false;
}
match build_new_window() {
Ok(_) => {
logging!(info, Type::Window, true, "新窗口创建成功");
}
Err(e) => {
logging!(error, Type::Window, true, "新窗口创建失败: {}", e);
return false;
}
}
if WindowOperationResult::Failed != Self::show_main_window().await {
return false;
}
handle::Handle::global().mark_startup_completed();
true
})
}
/// 摧毁窗口
pub fn destroy_main_window() -> WindowOperationResult {
if let Some(window) = Self::get_main_window() {
let _ = window.destroy();
logging!(info, Type::Window, true, "窗口已摧毁");
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
handle::Handle::global().set_activation_policy_accessory();
}
return WindowOperationResult::Destroyed;
}
logging!(warn, Type::Window, true, "窗口摧毁失败");
WindowOperationResult::Failed
}
/// 获取详细的窗口状态信息
+2 -5
View File
@@ -1,5 +1,5 @@
{
"version": "2.4.2",
"version": "2.4.3",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -33,10 +33,7 @@
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
],
"windows": {
"installMode": "basicUi"
+1 -3
View File
@@ -1,9 +1,7 @@
import { AppDataProvider } from "./providers/app-data-provider";
import Layout from "./pages/_layout";
import { useNotificationPermission } from "./hooks/useNotificationPermission";
import { AppDataProvider } from "./providers/app-data-provider";
function App() {
useNotificationPermission();
return (
<AppDataProvider>
<Layout />
+3 -2
View File
@@ -1,6 +1,7 @@
import React, { useSyncExternalStore } from "react";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import {
subscribeNotices,
hideNotice,
+3 -2
View File
@@ -1,4 +1,5 @@
import { ReactNode } from "react";
/* eslint-disable react/prop-types */
import { LoadingButton } from "@mui/lab";
import {
Button,
Dialog,
@@ -8,7 +9,7 @@ import {
type SxProps,
type Theme,
} from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { ReactNode } from "react";
interface Props {
title: ReactNode;
+1 -1
View File
@@ -1,5 +1,5 @@
import { alpha, Box, Typography } from "@mui/material";
import { InboxRounded } from "@mui/icons-material";
import { alpha, Box, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
interface Props {
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import { Box, styled } from "@mui/material";
import React from "react";
type Props = {
label: string;
+2 -4
View File
@@ -1,7 +1,7 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material";
import React from "react";
export interface BaseLoadingOverlayProps {
interface BaseLoadingOverlayProps {
isLoading: boolean;
}
@@ -29,5 +29,3 @@ export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
</Box>
);
};
export default BaseLoadingOverlay;
+3 -2
View File
@@ -1,7 +1,8 @@
import React, { ReactNode } from "react";
import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary";
import { useTheme } from "@mui/material/styles";
import React, { ReactNode } from "react";
import { BaseErrorBoundary } from "./base-error-boundary";
interface Props {
title?: React.ReactNode; // the page title
+4 -3
View File
@@ -1,10 +1,11 @@
import { Box, SvgIcon, TextField, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
import { useTranslation } from "react-i18next";
export type SearchState = {
text: string;
@@ -81,8 +82,8 @@ export const BaseSearchBox = (props: SearchProps) => {
return (content: string) => {
if (!searchText) return true;
let item = !matchCase ? content.toLowerCase() : content;
let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
const item = !matchCase ? content.toLowerCase() : content;
const searchItem = !matchCase ? searchText.toLowerCase() : searchText;
if (useRegularExpression) {
return new RegExp(searchItem).test(item);
+1 -1
View File
@@ -1,10 +1,10 @@
import { InfoRounded } from "@mui/icons-material";
import {
Tooltip,
IconButton,
IconButtonProps,
SvgIconProps,
} from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
interface Props extends IconButtonProps {
title?: string;
@@ -1,10 +1,10 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
import {
ErrorOutlineRounded,
RefreshRounded,
BugReportRounded,
} from "@mui/icons-material";
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
import React, { Component, ErrorInfo, ReactNode } from "react";
import { useTranslation } from "react-i18next";
interface Props {
@@ -1,10 +1,11 @@
import dayjs from "dayjs";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Snackbar, useTheme } from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { t } from "i18next";
import { forwardRef, useImperativeHandle, useState } from "react";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void;
@@ -1,5 +1,4 @@
import dayjs from "dayjs";
import { useLockFn } from "ahooks";
import { CloseRounded } from "@mui/icons-material";
import {
styled,
ListItem,
@@ -8,7 +7,9 @@ import {
Box,
alpha,
} from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
@@ -1,10 +1,11 @@
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
import dayjs from "dayjs";
import { useLocalStorage } from "foxact/use-local-storage";
import { t } from "i18next";
import { useMemo, useState } from "react";
import parseTraffic from "@/utils/parse-traffic";
import { truncateStr } from "@/utils/truncate-str";
interface Props {
connections: IConnectionsItem[];
+6 -4
View File
@@ -1,11 +1,13 @@
import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider } from "@mui/material";
import { DeveloperBoardOutlined } from "@mui/icons-material";
import { useClash } from "@/hooks/use-clash";
import { EnhancedCard } from "./enhanced-card";
import { Typography, Stack, Divider } from "@mui/material";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useClash } from "@/hooks/use-clash";
import { useAppData } from "@/providers/app-data-provider";
import { EnhancedCard } from "./enhanced-card";
// 将毫秒转换为时:分:秒格式的函数
const formatUptime = (uptimeMs: number) => {
const hours = Math.floor(uptimeMs / 3600000);
+7 -6
View File
@@ -1,16 +1,17 @@
import { useTranslation } from "react-i18next";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { useLockFn } from "ahooks";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import {
LanguageRounded,
MultipleStopRounded,
DirectionsRounded,
} from "@mui/icons-material";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { useLockFn } from "ahooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
export const ClashModeCard = () => {
const { t } = useTranslation();
+17 -16
View File
@@ -1,4 +1,15 @@
import { useTranslation } from "react-i18next";
import {
SignalWifi4Bar as SignalStrong,
SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium,
SignalWifi1Bar as SignalWeak,
SignalWifi0Bar as SignalNone,
WifiOff as SignalError,
ChevronRight,
SortRounded,
AccessTimeRounded,
SortByAlphaRounded,
} from "@mui/icons-material";
import {
Box,
Typography,
@@ -15,23 +26,13 @@ import {
IconButton,
} from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react";
import {
SignalWifi4Bar as SignalStrong,
SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium,
SignalWifi1Bar as SignalWeak,
SignalWifi0Bar as SignalNone,
WifiOff as SignalError,
ChevronRight,
SortRounded,
AccessTimeRounded,
SortByAlphaRounded,
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card";
import delayManager from "@/services/delay";
import { useAppData } from "@/providers/app-data-provider";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useAppData } from "@/providers/app-data-provider";
import delayManager from "@/services/delay";
// 本地存储的键名
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
@@ -44,7 +45,7 @@ interface ProxyOption {
}
// 排序类型: 默认 | 按延迟 | 按字母
export type ProxySortType = 0 | 1 | 2;
type ProxySortType = 0 | 1 | 2;
function convertDelayColor(delayValue: number) {
const colorStr = delayManager.formatDelayColor(delayValue);
@@ -1,3 +1,4 @@
import { Box, useTheme } from "@mui/material";
import {
forwardRef,
useImperativeHandle,
@@ -8,16 +9,16 @@ import {
useRef,
memo,
} from "react";
import { Box, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import parseTraffic from "@/utils/parse-traffic";
import {
useTrafficGraphDataEnhanced,
type ITrafficDataPoint,
} from "@/hooks/use-traffic-monitor";
import parseTraffic from "@/utils/parse-traffic";
// 流量数据项接口
export interface ITrafficItem {
interface ITrafficItem {
up: number;
down: number;
timestamp?: number;
+1 -1
View File
@@ -2,7 +2,7 @@ import { Box, Typography, alpha, useTheme } from "@mui/material";
import { ReactNode } from "react";
// 自定义卡片组件接口
export interface EnhancedCardProps {
interface EnhancedCardProps {
title: ReactNode;
icon: ReactNode;
action?: ReactNode;
+22 -20
View File
@@ -1,14 +1,3 @@
import { useRef, useCallback, memo, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
Typography,
Paper,
alpha,
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import {
ArrowUpwardRounded,
ArrowDownwardRounded,
@@ -17,19 +6,32 @@ import {
CloudUploadRounded,
CloudDownloadRounded,
} from "@mui/icons-material";
import {
Typography,
Paper,
alpha,
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import { useRef, useCallback, memo, useMemo } from "react";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { isDebugEnabled, gc } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import {
EnhancedCanvasTrafficGraph,
type EnhancedCanvasTrafficGraphRef,
} from "./enhanced-canvas-traffic-graph";
import { useVisibility } from "@/hooks/use-visibility";
import { useVerge } from "@/hooks/use-verge";
import parseTraffic from "@/utils/parse-traffic";
import { isDebugEnabled, gc } from "@/services/cmds";
import { ReactNode } from "react";
import { useAppData } from "@/providers/app-data-provider";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import useSWR from "swr";
interface StatCardProps {
icon: ReactNode;
+21 -19
View File
@@ -1,4 +1,12 @@
import { useTranslation } from "react-i18next";
import {
CloudUploadOutlined,
StorageOutlined,
UpdateOutlined,
DnsOutlined,
SpeedOutlined,
EventOutlined,
LaunchOutlined,
} from "@mui/icons-material";
import {
Box,
Typography,
@@ -10,24 +18,18 @@ import {
Link,
keyframes,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import {
CloudUploadOutlined,
StorageOutlined,
UpdateOutlined,
DnsOutlined,
SpeedOutlined,
EventOutlined,
LaunchOutlined,
} from "@mui/icons-material";
import dayjs from "dayjs";
import parseTraffic from "@/utils/parse-traffic";
import { useMemo, useCallback, useState } from "react";
import { openWebUrl, updateProfile } from "@/services/cmds";
import { useLockFn } from "ahooks";
import { showNotice } from "@/services/noticeService";
import { EnhancedCard } from "./enhanced-card";
import dayjs from "dayjs";
import { useMemo, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAppData } from "@/providers/app-data-provider";
import { openWebUrl, updateProfile } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";
import { EnhancedCard } from "./enhanced-card";
// 定义旋转动画
const round = keyframes`
@@ -55,7 +57,7 @@ interface ProfileExtra {
expire: number;
}
export interface ProfileItem {
interface ProfileItem {
uid: string;
type?: "local" | "remote" | "merge" | "script";
name?: string;
@@ -68,7 +70,7 @@ export interface ProfileItem {
option?: any;
}
export interface HomeProfileCardProps {
interface HomeProfileCardProps {
current: ProfileItem | null | undefined;
onProfileUpdated?: () => void;
}
+6 -4
View File
@@ -1,14 +1,16 @@
import { useTranslation } from "react-i18next";
import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material";
import {
LocationOnOutlined,
RefreshOutlined,
VisibilityOutlined,
VisibilityOffOutlined,
} from "@mui/icons-material";
import { EnhancedCard } from "./enhanced-card";
import { getIpInfo } from "@/services/api";
import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material";
import { useState, useEffect, useCallback, memo } from "react";
import { useTranslation } from "react-i18next";
import { getIpInfo } from "@/services/api";
import { EnhancedCard } from "./enhanced-card";
// 定义刷新时间(秒)
const IP_REFRESH_SECONDS = 300;
+10 -9
View File
@@ -1,4 +1,9 @@
import { useTranslation } from "react-i18next";
import {
ComputerRounded,
TroubleshootRounded,
HelpOutlineRounded,
SvgIconComponent,
} from "@mui/icons-material";
import {
Box,
Typography,
@@ -10,16 +15,12 @@ import {
Fade,
} from "@mui/material";
import { useState, useMemo, memo, FC } from "react";
import { useTranslation } from "react-i18next";
import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches";
import {
ComputerRounded,
TroubleshootRounded,
HelpOutlineRounded,
SvgIconComponent,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge";
import { useSystemState } from "@/hooks/use-system-state";
import { useSystemProxyState } from "@/hooks/use-system-proxy-state";
import { useSystemState } from "@/hooks/use-system-state";
import { useVerge } from "@/hooks/use-verge";
import { showNotice } from "@/services/noticeService";
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
+19 -17
View File
@@ -1,12 +1,3 @@
import { useTranslation } from "react-i18next";
import {
Typography,
Stack,
Divider,
Chip,
IconButton,
Tooltip,
} from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
@@ -15,18 +6,29 @@ import {
DnsOutlined,
ExtensionOutlined,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr";
import { getSystemInfo } from "@/services/cmds";
import { useNavigate } from "react-router-dom";
import { version as appVersion } from "@root/package.json";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Typography,
Stack,
Divider,
Chip,
IconButton,
Tooltip,
} from "@mui/material";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useLockFn } from "ahooks";
import { showNotice } from "@/services/noticeService";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import useSWR from "swr";
import { useSystemState } from "@/hooks/use-system-state";
import { useVerge } from "@/hooks/use-verge";
import { useServiceInstaller } from "@/hooks/useServiceInstaller";
import { getSystemInfo } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { version as appVersion } from "@root/package.json";
import { EnhancedCard } from "./enhanced-card";
export const SystemInfoCard = () => {
const { t } = useTranslation();
+9 -9
View File
@@ -1,6 +1,3 @@
import { useEffect, useRef, useMemo, useCallback } from "react";
import { useVerge } from "@/hooks/use-verge";
import { Box, IconButton, Tooltip, alpha, styled, Grid } from "@mui/material";
import {
DndContext,
closestCenter,
@@ -10,20 +7,23 @@ import {
DragEndEvent,
} from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
import { useTranslation } from "react-i18next";
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
import { TestItem } from "@/components/test/test-item";
import { Add, NetworkCheck } from "@mui/icons-material";
import { Box, IconButton, Tooltip, alpha, styled, Grid } from "@mui/material";
import { emit } from "@tauri-apps/api/event";
import { nanoid } from "nanoid";
import { Add, NetworkCheck } from "@mui/icons-material";
import { EnhancedCard } from "./enhanced-card";
import { useEffect, useRef, useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
// test icons
import apple from "@/assets/image/test/apple.svg?raw";
import github from "@/assets/image/test/github.svg?raw";
import google from "@/assets/image/test/google.svg?raw";
import youtube from "@/assets/image/test/youtube.svg?raw";
import { TestItem } from "@/components/test/test-item";
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card";
// 自定义滚动条样式
const ScrollBox = styled(Box)(({ theme }) => ({
+1
View File
@@ -6,6 +6,7 @@ import {
ListItemIcon,
} from "@mui/material";
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
import { useVerge } from "@/hooks/use-verge";
interface Props {
to: string;

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