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: contact_links:
- name: 讨论交流 / Communication - name: 讨论交流 / Communication
url: https://t.me/clash_verge_rev url: https://t.me/clash_verge_rev
+66 -23
View File
@@ -3,12 +3,14 @@ name: Auto Build
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
# UTC+8 0,6,12,18 # UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
- cron: "0 16,22,4,10 * * *" - cron: "0 4,10 * * *"
permissions: write-all permissions: write-all
env: env:
TAG_NAME: autobuild TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild TAG_CHANNEL: AutoBuild
PRE_TAG_NAME: pre-autobuild
PRE_TAG_CHANNEL: PreAutoBuild
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short RUST_BACKTRACE: short
concurrency: concurrency:
@@ -71,7 +73,7 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json) VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV 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 shell: bash
- run: | - run: |
@@ -99,7 +101,7 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### 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 ./路径 安装 #### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_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 }}. Created at ${{ env.BUILDTIME }}.
EOF EOF
- name: Upload Release - name: Upload Pre-Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
body_path: release.txt body_path: release.txt
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -130,7 +132,7 @@ jobs:
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with: with:
tag_name: autobuild tag_name: pre-autobuild
dry_run: false dry_run: false
autobuild-x86-windows-macos-linux: autobuild-x86-windows-macos-linux:
@@ -201,7 +203,7 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Tauri build for Windows-macOS-Linux - name: Tauri build for Windows-macOS-Linux
@@ -218,14 +220,13 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
tagName: ${{ env.TAG_NAME }} tagName: ${{ env.PRE_TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" releaseName: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
releaseBody: "More new features are now supported." releaseBody: "More new features are now supported."
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
# includeUpdaterJson: true
autobuild-arm-linux: autobuild-arm-linux:
name: Autobuild ARM Linux name: Autobuild ARM Linux
@@ -276,7 +277,7 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Setup for linux - name: Setup for linux
@@ -351,11 +352,11 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release - name: Upload Pre-Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: | files: |
@@ -408,7 +409,7 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
@@ -429,7 +430,6 @@ jobs:
with: with:
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
# includeUpdaterJson: true
- name: Rename - name: Rename
run: | run: |
@@ -451,20 +451,62 @@ jobs:
Rename-Item $file.FullName $newName Rename-Item $file.FullName $newName
} }
- name: Upload Release - name: Upload Pre-Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle - 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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: notify-telegram:
name: Notify Telegram name: Notify Telegram
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -474,6 +516,7 @@ jobs:
autobuild-x86-windows-macos-linux, autobuild-x86-windows-macos-linux,
autobuild-arm-linux, autobuild-arm-linux,
autobuild-x86-arm-windows_webview2, autobuild-x86-arm-windows_webview2,
promote-autobuild,
] ]
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -547,7 +590,7 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### 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 ./路径 安装 #### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_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 required: false
type: boolean type: boolean
default: true default: true
run_windows_arm64:
description: "运行 Windows ARM64"
required: false
type: boolean
default: true
permissions: write-all permissions: write-all
env: env:
@@ -40,6 +45,11 @@ jobs:
bundle: dmg bundle: dmg
id: macos-aarch64 id: macos-aarch64
input: run_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 }} runs-on: ${{ matrix.os }}
steps: steps:
+1
View File
@@ -10,3 +10,4 @@ scripts/_env.sh
.tool-versions .tool-versions
.idea .idea
.old .old
.eslintcache
+5 -2
View File
@@ -59,8 +59,9 @@ You have two options for downloading the clash binary:
```shell ```shell
pnpm run prebuild pnpm run prebuild
# Use '--force' to force update to the latest version # Use '--force' or '-f' to update both the Mihomo core version
# pnpm run prebuild --force # 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). - 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 pnpm dev
# If an app instance already exists, use a different command # If an app instance already exists, use a different command
pnpm dev:diff pnpm dev:diff
# To using tauri built-in dev tool
pnpm dev:tauri
``` ```
### Build the Project ### 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) | | 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) | | 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) | | AutoBuild | 滚动更新版,适合测试反馈,可能存在缺陷。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看 #### 安装说明和常见问题,请到 [文档页](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 ## v2.4.2
### ✨ 新增功能 ### ✨ 新增功能
@@ -7,7 +26,7 @@
### 🚀 性能优化 ### 🚀 性能优化
- 优化前端首页加载速度 - 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存呢 - 优化前端未使用 i18n 文件缓存
- 优化后端内存占用 - 优化后端内存占用
- 优化后端启动速度 - 优化后端启动速度
+97 -7
View File
@@ -1,16 +1,106 @@
import js from "@eslint/js"; 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 globals from "globals";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";
export default defineConfig([ export default defineConfig([
{ {
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: { js },
extends: ["js/recommended"], plugins: {
languageOptions: { globals: globals.browser }, 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", "name": "clash-verge",
"version": "2.4.2", "version": "2.4.3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace", "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": "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", "build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri", "tauri": "tauri",
@@ -24,7 +25,7 @@
"publish-version": "node scripts/publish-version.mjs", "publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --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": "prettier --write .",
"format:check": "prettier --check ." "format:check": "prettier --check ."
}, },
@@ -38,27 +39,25 @@
"@mui/icons-material": "^7.3.2", "@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.17", "@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.2", "@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/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2", "@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-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.1", "@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0", "@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.9.5", "ahooks": "^3.9.5",
"axios": "^1.11.0", "axios": "^1.12.2",
"cli-color": "^2.0.4",
"dayjs": "1.11.18", "dayjs": "1.11.18",
"foxact": "^0.2.49", "foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.5.2", "i18next": "^25.5.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.53.0",
"monaco-yaml": "^5.4.0", "monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"react": "19.1.1", "react": "19.1.1",
@@ -68,11 +67,9 @@
"react-i18next": "15.7.3", "react-i18next": "15.7.3",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0", "react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.2", "react-router-dom": "7.9.1",
"react-virtuoso": "^4.14.0", "react-virtuoso": "^4.14.0",
"sockette": "^2.0.6",
"swr": "^2.3.6", "swr": "^2.3.6",
"tar": "^7.4.3",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
@@ -82,28 +79,36 @@
"@tauri-apps/cli": "2.8.4", "@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "19.1.12", "@types/react": "19.1.13",
"@types/react-dom": "19.1.9", "@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.2", "@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"commander": "^14.0.0", "cli-color": "^2.0.4",
"commander": "^14.0.1",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"eslint": "^9.35.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", "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", "https-proxy-agent": "^7.0.6",
"jiti": "^2.5.1", "jiti": "^2.5.1",
"meta-json-schema": "^1.19.13", "meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"path": "^0.12.7",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"process": "^0.11.10",
"sass": "^1.92.1", "sass": "^1.92.1",
"tar": "^7.4.3",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.44.0",
"vite": "^7.1.4", "vite": "^7.1.5",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.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 cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge"); 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 = { const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32", "x86_64-pc-windows-msvc": "win32",
@@ -43,7 +43,8 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0]; const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1]; 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 const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] } ? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process; : process;
@@ -64,7 +65,7 @@ const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-v2", "win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386", "win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64", "win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2", "darwin-x64": "mihomo-darwin-amd64-v1",
"darwin-arm64": "mihomo-darwin-arm64", "darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2", "linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386", "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. * 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 fs from "fs/promises";
import path from "path"; import path from "path";
import { program } from "commander";
import { execSync } from "child_process";
/** /**
* 获取当前 git 短 commit hash * 获取当前 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] [package]
name = "clash-verge" name = "clash-verge"
version = "2.4.2" version = "2.4.3"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge" default-run = "clash-verge"
edition = "2021" edition = "2024"
build = "build.rs" build = "build.rs"
[package.metadata.bundle] [package.metadata.bundle]
@@ -22,12 +22,11 @@ dirs = "6.0"
open = "5.3.2" open = "5.3.2"
log = "0.4.28" log = "0.4.28"
dunce = "1.0.5" dunce = "1.0.5"
log4rs = "1.4.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.42"
sysinfo = { version = "0.37.0", features = ["network", "system"] } sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.143" serde_json = "1.0.145"
serde_yaml_ng = "0.10.0" serde_yaml_ng = "0.10.0"
once_cell = "1.21.3" once_cell = "1.21.3"
port_scanner = "0.1.5" port_scanner = "0.1.5"
@@ -40,7 +39,7 @@ tokio = { version = "1.47.1", features = [
"time", "time",
"sync", "sync",
] } ] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.225", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] } reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2" regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } 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-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0" tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.3" tauri-plugin-deep-link = "2.4.3"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0" tauri-plugin-window-state = "2.4.0"
zip = "5.0.0" zip = "5.1.1"
reqwest_dav = "0.2.2" reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
@@ -73,15 +71,21 @@ hmac = "0.12.1"
sha2 = "0.10.9" sha2 = "0.10.9"
hex = "0.4.3" hex = "0.4.3"
scopeguard = "1.2.0" scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc2" kode-bridge = "0.2.1"
dashmap = "6.1.0" dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1" tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [ isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding", "text-decoding",
"parking_lot", "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] [target.'cfg(windows)'.dependencies]
@@ -113,31 +117,38 @@ tauri-plugin-updater = "2.9.0"
[features] [features]
default = ["custom-protocol"] default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
verge-dev = [] verge-dev = ["nu-ansi-term"]
tauri-dev = []
tokio-trace = ["console-subscriber"] tokio-trace = ["console-subscriber"]
[profile.release] [profile.release]
panic = "abort" panic = "abort"
codegen-units = 1 codegen-units = 16
lto = true lto = true
opt-level = "s" opt-level = 2
debug = false
strip = true strip = true
overflow-checks = false
rpath = false
[profile.dev] [profile.dev]
incremental = true incremental = true
codegen-units = 256 # 增加编译单元,提升编译速度 codegen-units = 64
opt-level = 0 # 禁用优化,进一步提升编译速度 opt-level = 0
debug = true # 保留调试信息 debug = true
strip = false # 不剥离符号,保留调试信息 strip = "none"
overflow-checks = true
lto = false
rpath = false
[profile.fast-release] [profile.fast-release]
inherits = "release" # 继承 release 的配置 inherits = "release"
panic = "abort" # 与 release 相同 incremental = true
codegen-units = 256 # 增加编译单元,提升编译速度 codegen-units = 64
lto = false # 禁用 LTO,提升编译速度 lto = false
opt-level = 0 # 禁用优化,大幅提升编译速度 opt-level = 0
debug = true # 保留调试信息 debug = true
strip = false # 不剥离符号,保留调试信息 strip = false
[lib] [lib]
name = "app_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; use std::hint::black_box;
// 业务模型 & Draft // 业务模型 & Draft
+7 -1
View File
@@ -18,6 +18,12 @@
"autostart:allow-disable", "autostart:allow-disable",
"autostart:allow-is-enabled", "autostart:allow-is-enabled",
"core:window:allow-set-theme", "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 crate::singleton;
use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
pub struct CacheEntry { pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
pub value: Arc<Value>,
pub struct CacheEntry<T> {
pub value: Arc<T>,
pub expires_at: Instant, pub expires_at: Instant,
} }
pub struct ProxyRequestCache { pub struct Cache<T> {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry>>>>, pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
} }
impl ProxyRequestCache { impl<T> Cache<T> {
fn new() -> Self { fn new() -> Self {
ProxyRequestCache { Cache {
map: DashMap::new(), map: DashMap::new(),
} }
} }
@@ -27,10 +28,11 @@ impl ProxyRequestCache {
format!("{prefix}:{id}") 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 where
F: Fn() -> Fut + Send + 'static, F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Value> + Send + 'static, Fut: std::future::Future<Output = T> + Send + 'static,
T: Send + Sync + 'static,
{ {
loop { loop {
let now = Instant::now(); let now = Instant::now();
@@ -81,6 +83,10 @@ impl ProxyRequestCache {
} }
} }
// pub fn clean_key(&self, key: &str) {
// self.map.remove(key);
// }
// TODO // TODO
pub fn clean_default_keys(&self) { pub fn clean_default_keys(&self) {
// logging!(info, Type::Cache, "Cleaning proxies keys"); // logging!(info, Type::Cache, "Cleaning proxies keys");
@@ -98,5 +104,8 @@ impl ProxyRequestCache {
} }
} }
// Use singleton macro pub type CacheService = Cache<Result<String>>;
singleton!(ProxyRequestCache, INSTANCE); 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); crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(()) 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 super::CmdResult;
use crate::{ use crate::{
cache::CacheProxy,
config::Config, config::Config,
core::{handle, CoreManager}, core::{CoreManager, handle},
}; };
use crate::{ use crate::{
config::*, config::*,
feat, feat,
ipc::{self, IpcManager}, ipc::{self, IpcManager},
logging, logging,
state::proxy::ProxyRequestCache,
utils::logging::Type, utils::logging::Type,
wrap_err, 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 { pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, CoreManager}, core::{CoreManager, handle},
utils::dirs, utils::dirs,
}; };
@@ -317,8 +317,8 @@ pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
#[tauri::command] #[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> { pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global(); let manager = IpcManager::global();
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("clash_config", "default"); let key = CacheProxy::make_key("clash_config", "default");
let value = cache let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async { .get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| { manager.get_config().await.unwrap_or_else(|e| {
@@ -333,8 +333,8 @@ pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
/// 强制刷新Clash配置缓存 /// 强制刷新Clash配置缓存
#[tauri::command] #[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> { pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("clash_config", "default"); let key = CacheProxy::make_key("clash_config", "default");
cache.map.remove(&key); cache.map.remove(&key);
get_clash_config().await get_clash_config().await
} }
+26 -30
View File
@@ -618,20 +618,20 @@ async fn check_netflix(client: &Client) -> UnlockItem {
{ {
Ok(response) => { Ok(response) => {
// 检查重定向位置 // 检查重定向位置
if let Some(location) = response.headers().get("location") { if let Some(location) = response.headers().get("location")
if let Ok(location_str) = location.to_str() { && let Ok(location_str) = location.to_str()
// 解析位置获取区域 {
let parts: Vec<&str> = location_str.split('/').collect(); // 解析位置获取区域
if parts.len() >= 4 { let parts: Vec<&str> = location_str.split('/').collect();
let region_code = parts[3].split('-').next().unwrap_or("unknown"); if parts.len() >= 4 {
let emoji = country_code_to_emoji(region_code); let region_code = parts[3].split('-').next().unwrap_or("unknown");
return UnlockItem { let emoji = country_code_to_emoji(region_code);
name: "Netflix".to_string(), return UnlockItem {
status: "Yes".to_string(), name: "Netflix".to_string(),
region: Some(format!("{emoji}{region_code}")), status: "Yes".to_string(),
check_time: Some(get_local_date_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 { match response.json::<serde_json::Value>().await {
Ok(data) => { Ok(data) => {
// 尝试从数据中提取区域信息 // 尝试从数据中提取区域信息
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) { if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
if !targets.is_empty() { && !targets.is_empty()
if let Some(location) = targets[0].get("location") { && let Some(location) = targets[0].get("location")
if let Some(country) = && let Some(country) = location.get("country").and_then(|c| c.as_str())
location.get("country").and_then(|c| c.as_str()) {
{ let emoji = country_code_to_emoji(country);
let emoji = country_code_to_emoji(country); return UnlockItem {
return UnlockItem { name: "Netflix".to_string(),
name: "Netflix".to_string(), status: "Yes".to_string(),
status: "Yes".to_string(), region: Some(format!("{emoji}{country}")),
region: Some(format!("{emoji}{country}")), check_time: Some(get_local_date_string()),
check_time: Some(get_local_date_string()), };
};
}
}
}
} }
// 如果无法解析区域信息 // 如果无法解析区域信息
+1 -1
View File
@@ -1,5 +1,5 @@
use super::CmdResult; 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::process::AsyncHandler;
use crate::wrap_err; use crate::wrap_err;
use network_interface::NetworkInterface; use network_interface::NetworkInterface;
+95 -96
View File
@@ -1,13 +1,14 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
config::{ config::{
Config, IProfiles, PrfItem, PrfOption,
profiles::{ profiles::{
profiles_append_item_with_filedata_safe, profiles_delete_item_safe, profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_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, feat, logging,
process::AsyncHandler, process::AsyncHandler,
ret_err, ret_err,
@@ -290,109 +291,106 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile); logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误 // 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() { if let Some(new_profile) = profiles.current.as_ref()
if current_profile.as_ref() != Some(new_profile) { && current_profile.as_ref() != Some(new_profile)
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile); {
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径 // 获取目标配置文件路径
let config_file_result = { let config_file_result = {
let profiles_config = Config::profiles().await; let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref(); let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) { match profiles_data.get_item(new_profile) {
Ok(item) => { Ok(item) => {
if let Some(file) = &item.file { if let Some(file) = &item.file {
let path = dirs::app_profiles_dir().map(|dir| dir.join(file)); let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
path.ok() path.ok()
} else { } else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None None
} }
} }
}; Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
// 如果获取到文件路径,检查YAML语法 None
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);
} }
}
};
// 超时保护 // 如果获取到文件路径,检查YAML语法
let file_read_result = tokio::time::timeout( if let Some(file_path) = config_file_result {
Duration::from_secs(5), if !file_path.exists() {
tokio::fs::read_to_string(&file_path), logging!(
) error,
.await; 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 file_read_result = tokio::time::timeout(
let yaml_parse_result = AsyncHandler::spawn_blocking(move || { Duration::from_secs(5),
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) tokio::fs::read_to_string(&file_path),
}) )
.await; .await;
match yaml_parse_result { match file_read_result {
Ok(Ok(_)) => { Ok(Ok(content)) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确"); let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
} serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
Ok(Err(err)) => { })
let error_msg = format!(" {err}"); .await;
logging!(
error, match yaml_parse_result {
Type::Cmd, Ok(Ok(_)) => {
true, logging!(info, Type::Cmd, true, "目标配置文件语法正确");
"目标配置文件存在YAML语法错误:{}", }
error_msg Ok(Err(err)) => {
); let error_msg = format!(" {err}");
handle::Handle::notice_message( logging!(
"config_validate::yaml_syntax_error", error,
&error_msg, Type::Cmd,
); true,
return Ok(false); "目标配置文件存在YAML语法错误:{}",
} error_msg
Err(join_err) => { );
let error_msg = format!("YAML解析任务失败: {join_err}"); handle::Handle::notice_message(
logging!(error, Type::Cmd, true, "{}", error_msg); "config_validate::yaml_syntax_error",
handle::Handle::notice_message( &error_msg,
"config_validate::yaml_parse_error", );
&error_msg, return Ok(false);
); }
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}"); Ok(Err(err)) => {
logging!(error, Type::Cmd, true, "{}", error_msg); let error_msg = format!("无法读取目标配置文件: {err}");
handle::Handle::notice_message( logging!(error, Type::Cmd, true, "{}", error_msg);
"config_validate::file_read_error", handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
&error_msg, return Ok(false);
); }
return Ok(false); Err(_) => {
} let error_msg = "读取配置文件超时(5秒)".to_string();
Err(_) => { logging!(error, Type::Cmd, true, "{}", error_msg);
let error_msg = "读取配置文件超时(5秒)".to_string(); handle::Handle::notice_message(
logging!(error, Type::Cmd, true, "{}", error_msg); "config_validate::file_read_timeout",
handle::Handle::notice_message( &error_msg,
"config_validate::file_read_timeout", );
&error_msg, return Ok(false);
);
return Ok(false);
}
} }
} }
} }
@@ -663,8 +661,9 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command] #[tauri::command]
pub async fn view_profile(index: String) -> CmdResult { pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let file = { let file = {
wrap_err!(profiles.latest_ref().get_item(&index))? wrap_err!(profiles_ref.get_item(&index))?
.file .file
.clone() .clone()
.ok_or("the file field is null") .ok_or("the file field is null")
+11 -9
View File
@@ -2,10 +2,10 @@ use tauri::Emitter;
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
cache::CacheProxy,
core::{handle::Handle, tray::Tray}, core::{handle::Handle, tray::Tray},
ipc::IpcManager, ipc::IpcManager,
logging, logging,
state::proxy::ProxyRequestCache,
utils::logging::Type, utils::logging::Type,
}; };
use std::time::Duration; use std::time::Duration;
@@ -15,8 +15,8 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("proxies", "default"); let key = CacheProxy::make_key("proxies", "default");
let value = cache let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global(); let manager = IpcManager::global();
@@ -32,16 +32,16 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
/// 强制刷新代理缓存用于profile切换 /// 强制刷新代理缓存用于profile切换
#[tauri::command] #[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> { pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("proxies", "default"); let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key); cache.map.remove(&key);
get_proxies().await get_proxies().await
} }
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("providers", "default"); let key = CacheProxy::make_key("providers", "default");
let value = cache let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global(); 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<()> { pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
match IpcManager::global().update_proxy(&group, &proxy).await { match IpcManager::global().update_proxy(&group, &proxy).await {
Ok(_) => { Ok(_) => {
// println!("Proxy updated successfully: {} -> {}", group,proxy);
logging!( logging!(
info, info,
Type::Cmd, Type::Cmd,
@@ -84,8 +85,8 @@ pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()
proxy proxy
); );
let cache = crate::state::proxy::ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = crate::state::proxy::ProxyRequestCache::make_key("proxies", "default"); let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key); cache.map.remove(&key);
if let Err(e) = Tray::global().update_menu().await { 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(()) Ok(())
} }
Err(e) => { Err(e) => {
println!("1111111111111111");
logging!( logging!(
error, error,
Type::Cmd, Type::Cmd,
+95 -6
View File
@@ -1,5 +1,5 @@
use super::CmdResult; use super::CmdResult;
use crate::{config::*, wrap_err}; use crate::{config::*, core::CoreManager, log_err, wrap_err};
use anyhow::Context; use anyhow::Context;
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use std::collections::HashMap; 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> { pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await; let runtime = Config::runtime().await;
let runtime = runtime.latest_ref(); let runtime = runtime.latest_ref();
let config = runtime.config.as_ref(); let config = runtime.config.as_ref();
wrap_err!(config wrap_err!(
.ok_or(anyhow::anyhow!("failed to parse config to yaml file")) config
.and_then( .ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|config| serde_yaml_ng::to_string(config).context("failed to convert config to yaml") .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)>>> { pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_ref().chain_logs.clone()) 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 super::CmdResult;
use crate::{ use crate::{
core::{service, CoreManager}, core::{
CoreManager,
service::{self, SERVICE_MANAGER, ServiceStatus},
},
utils::i18n::t, utils::i18n::t,
}; };
use anyhow::Result;
async fn execute_service_operation_sync<F, Fut, E>(service_op: F, op_type: &str) -> CmdResult async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
where if let Err(e) = SERVICE_MANAGER
F: FnOnce() -> Fut, .lock()
Fut: std::future::Future<Output = Result<(), E>>, .await
E: ToString + std::fmt::Debug, .handle_service_status(&status)
{ .await
if let Err(e) = service_op().await { {
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string()); let emsg = format!("{} Service failed: {}", op_type, e);
return Err(t(emsg.as_str()).await); return Err(t(emsg.as_str()).await);
} }
if CoreManager::global().restart_core().await.is_err() { if CoreManager::global().restart_core().await.is_err() {
let emsg = format!("{} {} failed", "Restart", "Core"); let emsg = "Restart Core failed";
return Err(t(emsg.as_str()).await); return Err(t(emsg).await);
} }
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult { 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] #[tauri::command]
pub async fn uninstall_service() -> CmdResult { 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] #[tauri::command]
pub async fn reinstall_service() -> CmdResult { 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] #[tauri::command]
pub async fn repair_service() -> CmdResult { 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] #[tauri::command]
+1 -1
View File
@@ -1,6 +1,6 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
core::{handle, CoreManager}, core::{CoreManager, handle},
logging, logging,
module::sysinfo::PlatformSpecification, module::sysinfo::PlatformSpecification,
utils::logging::Type, utils::logging::Type,
+8 -8
View File
@@ -25,17 +25,17 @@ impl IClashTemp {
match map_result { match map_result {
Ok(mut map) => { Ok(mut map) => {
template.0.keys().for_each(|key| { template.0.keys().for_each(|key| {
if !map.contains_key(key) { if !map.contains_key(key)
if let Some(value) = template.0.get(key) { && let Some(value) = template.0.get(key)
map.insert(key.clone(), value.clone()); {
} map.insert(key.clone(), value.clone());
} }
}); });
// 确保 secret 字段存在且不为空 // 确保 secret 字段存在且不为空
if let Some(Value::String(s)) = map.get_mut("secret") { if let Some(Value::String(s)) = map.get_mut("secret")
if s.is_empty() { && s.is_empty()
*s = "set-your-secret".to_string(); {
} *s = "set-your-secret".to_string();
} }
Self(Self::guard(map)) Self(Self::guard(map))
} }
+7 -5
View File
@@ -1,14 +1,14 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{ use crate::{
config::{profiles_append_item_safe, PrfItem}, config::{PrfItem, profiles_append_item_safe},
core::{handle, CoreManager}, core::{CoreManager, handle},
enhance, logging, enhance, logging,
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
}; };
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
use std::path::PathBuf; use std::path::PathBuf;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use tokio::time::{sleep, Duration}; use tokio::time::{Duration, sleep};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -101,7 +101,9 @@ impl Config {
Some(("config_validate::boot_error", error_msg)) Some(("config_validate::boot_error", error_msg))
} else { } else {
logging!(info, Type::Config, true, "配置验证成功"); logging!(info, Type::Config, true, "配置验证成功");
Some(("config_validate::success", String::new())) // 前端没有必要知道验证成功的消息,也没有事件驱动
// Some(("config_validate::success", String::new()))
None
} }
} }
Err(err) => { Err(err) => {
+2 -2
View File
@@ -1,9 +1,9 @@
use crate::utils::dirs::get_encryption_key; use crate::utils::dirs::get_encryption_key;
use aes_gcm::{ use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key, 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}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
const NONCE_LENGTH: usize = 12; const NONCE_LENGTH: usize = 12;
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::utils::{
network::{NetworkManager, ProxyType}, network::{NetworkManager, ProxyType},
tmpl, tmpl,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use std::{fs, time::Duration}; 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::{ use crate::{
logging_error, logging_error,
process::AsyncHandler, process::AsyncHandler,
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
}; };
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use std::collections::HashSet; use std::collections::HashSet;
@@ -88,12 +88,12 @@ impl IProfiles {
self.items = Some(vec![]); self.items = Some(vec![]);
} }
if let Some(current) = patch.current { if let Some(current) = patch.current
if let Some(items) = self.items.as_ref() { && let Some(items) = self.items.as_ref()
let some_uid = Some(current); {
if items.iter().any(|e| e.uid == some_uid) { let some_uid = Some(current);
self.current = some_uid; if items.iter().any(|e| e.uid == some_uid) {
} self.current = some_uid;
} }
} }
@@ -287,24 +287,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = index { if let Some(index) = index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(async move |path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let result = fs::remove_file(path.clone()).await; if path.exists() {
if let Err(err) = result { let result = fs::remove_file(path.clone()).await;
logging_error!( if let Err(err) = result {
Type::Config, logging_error!(
false, Type::Config,
"[配置文件删除] 删除文件 {} 失败: {}", false,
path.display(), "[配置文件删除] 删除文件 {} 失败: {}",
err path.display(),
); err
} );
} }
}); }
} });
} }
// get the merge index // get the merge index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -313,24 +313,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = merge_index { if let Some(index) = merge_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(async move |path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let result = fs::remove_file(path.clone()).await; if path.exists() {
if let Err(err) = result { let result = fs::remove_file(path.clone()).await;
logging_error!( if let Err(err) = result {
Type::Config, logging_error!(
false, Type::Config,
"[配置文件删除] 删除文件 {} 失败: {}", false,
path.display(), "[配置文件删除] 删除文件 {} 失败: {}",
err path.display(),
); err
} );
} }
}); }
} });
} }
// get the script index // get the script index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -339,24 +339,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = script_index { if let Some(index) = script_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(async move |path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let result = fs::remove_file(path.clone()).await; if path.exists() {
if let Err(err) = result { let result = fs::remove_file(path.clone()).await;
logging_error!( if let Err(err) = result {
Type::Config, logging_error!(
false, Type::Config,
"[配置文件删除] 删除文件 {} 失败: {}", false,
path.display(), "[配置文件删除] 删除文件 {} 失败: {}",
err path.display(),
); err
} );
} }
}); }
} });
} }
// get the rules index // get the rules index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -365,24 +365,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = rules_index { if let Some(index) = rules_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(async move |path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let result = fs::remove_file(path.clone()).await; if path.exists() {
if let Err(err) = result { let result = fs::remove_file(path.clone()).await;
logging_error!( if let Err(err) = result {
Type::Config, logging_error!(
false, Type::Config,
"[配置文件删除] 删除文件 {} 失败: {}", false,
path.display(), "[配置文件删除] 删除文件 {} 失败: {}",
err path.display(),
); err
} );
} }
}); }
} });
} }
// get the proxies index // get the proxies index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -391,24 +391,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = proxies_index { if let Some(index) = proxies_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(async move |path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let result = fs::remove_file(path.clone()).await; if path.exists() {
if let Err(err) = result { let result = fs::remove_file(path.clone()).await;
logging_error!( if let Err(err) = result {
Type::Config, logging_error!(
false, Type::Config,
"[配置文件删除] 删除文件 {} 失败: {}", false,
path.display(), "[配置文件删除] 删除文件 {} 失败: {}",
err path.display(),
); err
} );
} }
}); }
} });
} }
// get the groups index // get the groups index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -417,24 +417,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = groups_index { if let Some(index) = groups_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(async move |path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let result = fs::remove_file(path.clone()).await; if path.exists() {
if let Err(err) = result { let result = fs::remove_file(path.clone()).await;
logging_error!( if let Err(err) = result {
Type::Config, logging_error!(
false, Type::Config,
"[配置文件删除] 删除文件 {} 失败: {}", false,
path.display(), "[配置文件删除] 删除文件 {} 失败: {}",
err path.display(),
); err
} );
} }
}); }
} });
} }
// delete the original uid // delete the original uid
if current == uid { if current == uid {
@@ -595,25 +595,25 @@ impl IProfiles {
total_files += 1; total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
if Self::is_profile_file(file_name) { && Self::is_profile_file(file_name)
// 检查是否为全局扩展文件 {
if protected_files.contains(file_name) { // 检查是否为全局扩展文件
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}"); if protected_files.contains(file_name) {
continue; log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
} continue;
}
// 检查是否为活跃文件 // 检查是否为活跃文件
if !active_files.contains(file_name) { if !active_files.contains(file_name) {
match std::fs::remove_file(&path) { match std::fs::remove_file(&path) {
Ok(_) => { Ok(_) => {
deleted_files.push(file_name.to_string()); deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}"); log::info!(target: "app", "已清理冗余文件: {file_name}");
} }
Err(e) => { Err(e) => {
failed_deletions.push(format!("{file_name}: {e}")); failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}"); log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
} }
} }
} }
@@ -659,50 +659,44 @@ impl IProfiles {
} }
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件 // 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype { if let Some(itype) = &item.itype
if itype == "remote" || itype == "local" { && (itype == "remote" || itype == "local")
if let Some(option) = &item.option { && 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(merge_uid) = &option.merge
if let Some(file) = &merge_item.file { && let Ok(merge_item) = self.get_item(merge_uid)
active_files.insert(file.clone()); && let Some(file) = &merge_item.file
} {
} active_files.insert(file.clone());
} }
if let Some(script_uid) = &option.script { if let Some(script_uid) = &option.script
if let Ok(script_item) = self.get_item(script_uid) { && let Ok(script_item) = self.get_item(script_uid)
if let Some(file) = &script_item.file { && let Some(file) = &script_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
} }
}
if let Some(rules_uid) = &option.rules { if let Some(rules_uid) = &option.rules
if let Ok(rules_item) = self.get_item(rules_uid) { && let Ok(rules_item) = self.get_item(rules_uid)
if let Some(file) = &rules_item.file { && let Some(file) = &rules_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
} }
}
if let Some(proxies_uid) = &option.proxies { if let Some(proxies_uid) = &option.proxies
if let Ok(proxies_item) = self.get_item(proxies_uid) { && let Ok(proxies_item) = self.get_item(proxies_uid)
if let Some(file) = &proxies_item.file { && let Some(file) = &proxies_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
} }
}
if let Some(groups_uid) = &option.groups { if let Some(groups_uid) = &option.groups
if let Ok(groups_item) = self.get_item(groups_uid) { && let Ok(groups_item) = self.get_item(groups_uid)
if let Some(file) = &groups_item.file { && let Some(file) = &groups_item.file
active_files.insert(file.clone()); {
} 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::{ use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC}, config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging, logging,
utils::{dirs, help, i18n, logging::Type}, utils::{dirs, help, i18n, logging::Type},
}; };
@@ -203,9 +203,6 @@ pub struct IVerge {
/// 启用外部控制器 /// 启用外部控制器
pub enable_external_controller: Option<bool>, pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -407,7 +404,6 @@ impl IVerge {
auto_light_weight_minutes: Some(10), auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(false), enable_dns_settings: Some(false),
home_cards: None, home_cards: None,
service_state: None,
enable_external_controller: Some(false), enable_external_controller: Some(false),
..Self::default() ..Self::default()
} }
@@ -495,7 +491,6 @@ impl IVerge {
patch!(auto_light_weight_minutes); patch!(auto_light_weight_minutes);
patch!(enable_dns_settings); patch!(enable_dns_settings);
patch!(home_cards); patch!(home_cards);
patch!(service_state);
patch!(enable_external_controller); patch!(enable_external_controller);
} }
@@ -592,7 +587,6 @@ pub struct IVergeResponse {
pub home_cards: Option<serde_json::Value>, pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>, pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>, pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
} }
impl From<IVerge> for IVergeResponse { impl From<IVerge> for IVergeResponse {
@@ -664,7 +658,6 @@ impl From<IVerge> for IVergeResponse {
home_cards: verge.home_cards, home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator, enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
enable_external_controller: verge.enable_external_controller, 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 crate::process::AsyncHandler;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration}; use tokio::time::{Duration, timeout};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use anyhow::anyhow; use anyhow::anyhow;
@@ -87,7 +87,7 @@ impl AsyncProxyQuery {
use std::ptr; use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; 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 { unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -209,13 +209,13 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和GNOME设置 // Linux: 检查环境变量和GNOME设置
// 首先检查环境变量 // 首先检查环境变量
if let Ok(auto_proxy) = std::env::var("auto_proxy") { if let Ok(auto_proxy) = std::env::var("auto_proxy")
if !auto_proxy.is_empty() { && !auto_proxy.is_empty()
return Ok(AsyncAutoproxy { {
enable: true, return Ok(AsyncAutoproxy {
url: auto_proxy, enable: true,
}); url: auto_proxy,
} });
} }
// 尝试使用 gsettings 获取 GNOME 代理设置 // 尝试使用 gsettings 获取 GNOME 代理设置
@@ -224,31 +224,31 @@ impl AsyncProxyQuery {
.output() .output()
.await; .await;
if let Ok(output) = output { if let Ok(output) = output
if output.status.success() { && output.status.success()
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); {
if mode.contains("auto") { let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
// 获取 PAC URL if mode.contains("auto") {
let pac_output = Command::new("gsettings") // 获取 PAC URL
.args(["get", "org.gnome.system.proxy", "autoconfig-url"]) let pac_output = Command::new("gsettings")
.output() .args(["get", "org.gnome.system.proxy", "autoconfig-url"])
.await; .output()
.await;
if let Ok(pac_output) = pac_output { if let Ok(pac_output) = pac_output
if pac_output.status.success() { && pac_output.status.success()
let pac_url = String::from_utf8_lossy(&pac_output.stdout) {
.trim() let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim_matches('\'') .trim()
.trim_matches('"') .trim_matches('\'')
.to_string(); .trim_matches('"')
.to_string();
if !pac_url.is_empty() { if !pac_url.is_empty() {
return Ok(AsyncAutoproxy { return Ok(AsyncAutoproxy {
enable: true, enable: true,
url: pac_url, url: pac_url,
}); });
}
}
} }
} }
} }
@@ -271,7 +271,7 @@ impl AsyncProxyQuery {
use std::ptr; use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; 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 { unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -402,10 +402,10 @@ impl AsyncProxyQuery {
http_host = host_part.trim().to_string(); http_host = host_part.trim().to_string();
} }
} else if line.contains("HTTPPort") { } else if line.contains("HTTPPort") {
if let Some(port_part) = line.split(':').nth(1) { if let Some(port_part) = line.split(':').nth(1)
if let Ok(port) = port_part.trim().parse::<u16>() { && let Ok(port) = port_part.trim().parse::<u16>()
http_port = port; {
} http_port = port;
} }
} else if line.contains("ExceptionsList") { } else if line.contains("ExceptionsList") {
// 解析异常列表 // 解析异常列表
@@ -431,16 +431,16 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和桌面环境设置 // Linux: 检查环境变量和桌面环境设置
// 首先检查环境变量 // 首先检查环境变量
if let Ok(http_proxy) = std::env::var("http_proxy") { if let Ok(http_proxy) = std::env::var("http_proxy")
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) { && let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy)
return Ok(proxy_info); {
} return Ok(proxy_info);
} }
if let Ok(https_proxy) = std::env::var("https_proxy") { if let Ok(https_proxy) = std::env::var("https_proxy")
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) { && let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy)
return Ok(proxy_info); {
} return Ok(proxy_info);
} }
// 尝试使用 gsettings 获取 GNOME 代理设置 // 尝试使用 gsettings 获取 GNOME 代理设置
@@ -449,45 +449,46 @@ impl AsyncProxyQuery {
.output() .output()
.await; .await;
if let Ok(mode_output) = mode_output { if let Ok(mode_output) = mode_output
if mode_output.status.success() { && mode_output.status.success()
let mode = String::from_utf8_lossy(&mode_output.stdout) {
.trim() let mode = String::from_utf8_lossy(&mode_output.stdout)
.to_string(); .trim()
if mode.contains("manual") { .to_string();
// 获取HTTP代理设置 if mode.contains("manual") {
let host_result = Command::new("gsettings") // 获取HTTP代理设置
.args(["get", "org.gnome.system.proxy.http", "host"]) let host_result = Command::new("gsettings")
.output() .args(["get", "org.gnome.system.proxy.http", "host"])
.await; .output()
.await;
let port_result = Command::new("gsettings") let port_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "port"]) .args(["get", "org.gnome.system.proxy.http", "port"])
.output() .output()
.await; .await;
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) { if let (Ok(host_output), Ok(port_output)) = (host_result, port_result)
if host_output.status.success() && port_output.status.success() { && host_output.status.success()
let host = String::from_utf8_lossy(&host_output.stdout) && port_output.status.success()
.trim() {
.trim_matches('\'') let host = String::from_utf8_lossy(&host_output.stdout)
.trim_matches('"') .trim()
.to_string(); .trim_matches('\'')
.trim_matches('"')
.to_string();
let port = String::from_utf8_lossy(&port_output.stdout) let port = String::from_utf8_lossy(&port_output.stdout)
.trim() .trim()
.parse::<u16>() .parse::<u16>()
.unwrap_or(8080); .unwrap_or(8080);
if !host.is_empty() { if !host.is_empty() {
return Ok(AsyncSysproxy { return Ok(AsyncSysproxy {
enable: true, enable: true,
host, host,
port, port,
bypass: String::new(), bypass: String::new(),
}); });
}
}
} }
} }
} }
+69 -263
View File
@@ -1,13 +1,13 @@
#[cfg(target_os = "windows")]
use crate::AsyncHandler;
use crate::{ use crate::{
config::*, config::*,
core::{ core::{
handle, handle,
service::{self}, service::{self, SERVICE_MANAGER, ServiceStatus},
}, },
ipc::IpcManager, ipc::IpcManager,
logging, logging_error, logging, logging_error, singleton_lazy,
process::AsyncHandler,
singleton_lazy,
utils::{ utils::{
dirs, dirs,
help::{self}, help::{self},
@@ -19,12 +19,12 @@ use chrono::Local;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
fmt, fmt,
fs::{create_dir_all, File}, fs::{File, create_dir_all},
io::Write, io::Write,
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
}; };
use tauri_plugin_shell::{process::CommandChild, ShellExt}; use tauri_plugin_shell::{ShellExt, process::CommandChild};
#[derive(Debug)] #[derive(Debug)]
pub struct CoreManager { pub struct CoreManager {
@@ -384,8 +384,6 @@ impl CoreManager {
return Ok((true, String::new())); return Ok((true, String::new()));
} }
logging!(info, Type::Config, true, "开始更新配置");
// 1. 先生成新的配置内容 // 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容"); logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate().await?; Config::generate().await?;
@@ -393,9 +391,8 @@ impl CoreManager {
// 2. 验证配置 // 2. 验证配置
match self.validate_config().await { match self.validate_config().await {
Ok((true, _)) => { Ok((true, _)) => {
logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置 // 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "生成运行时配置"); logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run).await?; let run_path = Config::generate_file(ConfigType::Run).await?;
logging_error!(Type::Config, true, self.put_configs_force(run_path).await); logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
Ok((true, "something".into())) Ok((true, "something".into()))
@@ -467,18 +464,18 @@ impl CoreManager {
Ok((pids, process_name)) => { Ok((pids, process_name)) => {
for pid in pids { for pid in pids {
// 跳过当前管理的进程 // 跳过当前管理的进程
if let Some(current) = current_pid { if let Some(current) = current_pid
if pid == current { && pid == current
logging!( {
debug, logging!(
Type::Core, debug,
true, Type::Core,
"跳过当前管理的进程: {} (PID: {})", true,
process_name, "跳过当前管理的进程: {} (PID: {})",
pid process_name,
); pid
continue; );
} continue;
} }
pids_to_kill.push((pid, process_name.clone())); pids_to_kill.push((pid, process_name.clone()));
} }
@@ -527,7 +524,7 @@ impl CoreManager {
use std::mem; use std::mem;
use winapi::um::handleapi::CloseHandle; use winapi::um::handleapi::CloseHandle;
use winapi::um::tlhelp32::{ use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS, TH32CS_SNAPPROCESS,
}; };
use winapi::um::winnt::HANDLE; use winapi::um::winnt::HANDLE;
@@ -703,7 +700,7 @@ impl CoreManager {
use winapi::um::processthreadsapi::OpenProcess; use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION}; use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = AsyncHandler::spawn_blocking(move || -> Result<bool> { AsyncHandler::spawn_blocking(move || -> Result<bool> {
unsafe { unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() { if process_handle.is_null() {
@@ -719,9 +716,7 @@ impl CoreManager {
Ok(exit_code == 259) Ok(exit_code == 259)
} }
}) })
.await?; .await?
result
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -736,7 +731,8 @@ impl CoreManager {
} }
async fn start_core_by_sidecar(&self) -> Result<()> { 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 config_file = &Config::generate_file(ConfigType::Run).await?;
let app_handle = handle::Handle::global() let app_handle = handle::Handle::global()
.app_handle() .app_handle()
@@ -765,21 +761,19 @@ impl CoreManager {
]) ])
.spawn()?; .spawn()?;
AsyncHandler::spawn(move || async move { while let Some(event) = rx.recv().await {
while let Some(event) = rx.recv().await { if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event { && let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line))
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) { {
logging!( logging!(
error, error,
Type::Core, Type::Core,
true, true,
"[Sidecar] Failed to write stdout to file: {}", "[Sidecar] Failed to write stdout to file: {}",
e e
); );
}
}
} }
}); }
let pid = child.pid(); let pid = child.pid();
logging!( logging!(
@@ -794,7 +788,7 @@ impl CoreManager {
Ok(()) Ok(())
} }
fn stop_core_by_sidecar(&self) -> Result<()> { 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() { if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid(); let pid = child.pid();
@@ -814,14 +808,14 @@ impl CoreManager {
impl CoreManager { impl CoreManager {
async fn start_core_by_service(&self) -> Result<()> { 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?; let config_file = &Config::generate_file(ConfigType::Run).await?;
service::run_core_by_service(config_file).await?; service::run_core_by_service(config_file).await?;
self.set_running_mode(RunningMode::Service); self.set_running_mode(RunningMode::Service);
Ok(()) Ok(())
} }
async fn stop_core_by_service(&self) -> Result<()> { 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?; service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning); self.set_running_mode(RunningMode::NotRunning);
Ok(()) Ok(())
@@ -837,58 +831,9 @@ impl Default for CoreManager {
} }
} }
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
impl CoreManager { 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<()> { pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core"); logging!(info, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程 // 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { 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, "核心初始化完成");
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);
Ok(()) Ok(())
} }
@@ -1063,31 +864,32 @@ impl CoreManager {
(*guard).clone() (*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<()> { pub async fn start_core(&self) -> Result<()> {
if service::is_service_available().await.is_ok() { self.prestart_core().await?;
if service::check_service_needs_reinstall().await {
service::reinstall_service().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(()) Ok(())
} }
@@ -1102,6 +904,7 @@ impl CoreManager {
/// 重启内核 /// 重启内核
pub async fn restart_core(&self) -> Result<()> { pub async fn restart_core(&self) -> Result<()> {
logging!(info, Type::Core, true, "Restarting core");
self.stop_core().await?; self.stop_core().await?;
self.start_core().await?; self.start_core().await?;
Ok(()) Ok(())
@@ -1143,3 +946,6 @@ impl CoreManager {
Ok(()) 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 std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration}; use tokio::time::{Duration, sleep, timeout};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge}; use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery; use crate::core::async_proxy_query::AsyncProxyQuery;
+20 -37
View File
@@ -2,8 +2,9 @@ use crate::singleton;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::{ use std::{
sync::{ sync::{
Arc,
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
mpsc, Arc, mpsc,
}, },
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
@@ -20,7 +21,6 @@ enum FrontendEvent {
NoticeMessage { status: String, message: String }, NoticeMessage { status: String, message: String },
ProfileChanged { current_profile_id: String }, ProfileChanged { current_profile_id: String },
TimerUpdated { profile_index: String }, TimerUpdated { profile_index: String },
StartupCompleted,
ProfileUpdateStarted { uid: String }, ProfileUpdateStarted { uid: String },
ProfileUpdateCompleted { uid: String }, ProfileUpdateCompleted { uid: String },
} }
@@ -98,16 +98,14 @@ impl NotificationSystem {
let is_emergency = *system.emergency_mode.read(); let is_emergency = *system.emergency_mode.read();
if is_emergency { if is_emergency
if let FrontendEvent::NoticeMessage { ref status, .. } = event { && let FrontendEvent::NoticeMessage { ref status, .. } = event
if status == "info" { && status == "info" {
log::warn!( log::warn!(
"Emergency mode active, skipping info message" "Emergency mode active, skipping info message"
); );
continue; continue;
} }
}
}
if let Some(window) = handle.get_window() { if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now(); *system.last_emit_time.write() = Instant::now();
@@ -134,9 +132,6 @@ impl NotificationSystem {
FrontendEvent::TimerUpdated { profile_index } => { FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index))) ("verge://timer-updated", Ok(serde_json::json!(profile_index)))
} }
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => { FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid }))) ("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
} }
@@ -203,13 +198,12 @@ impl NotificationSystem {
/// 发送事件到队列 /// 发送事件到队列
fn send_event(&self, event: FrontendEvent) -> bool { fn send_event(&self, event: FrontendEvent) -> bool {
if *self.emergency_mode.read() { if *self.emergency_mode.read()
if let FrontendEvent::NoticeMessage { ref status, .. } = event { && let FrontendEvent::NoticeMessage { ref status, .. } = event
if status == "info" { && status == "info"
log::info!("Skipping info message in emergency mode"); {
return false; log::info!("Skipping info message in emergency mode");
} return false;
}
} }
if let Some(sender) = &self.sender { 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) { pub fn notify_profile_update_started(uid: String) {
let handle = Self::global(); let handle = Self::global();
if handle.is_exiting() { if handle.is_exiting() {
@@ -392,7 +370,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() { if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateStarted { uid }); system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
} else { } 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() { if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid }); system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
} else { } 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, info,
Type::Frontend, Type::Frontend,
true, true,
"发送{}条启动时累积的错误消息", "发送{}条启动时累积的错误消息: {:?}",
errors.len() errors.len(),
errors
); );
// 启动单独线程处理启动错误,避免阻塞主线程 // 启动单独线程处理启动错误,避免阻塞主线程
+8 -8
View File
@@ -1,10 +1,10 @@
use crate::process::AsyncHandler; use crate::process::AsyncHandler;
use crate::utils::notification::{notify_event, NotificationEvent}; use crate::utils::notification::{NotificationEvent, notify_event};
use crate::{ use crate::{
config::Config, core::handle, feat, logging, logging_error, config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type, module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
@@ -109,7 +109,7 @@ impl Hotkey {
match function { match function {
HotkeyFunction::OpenOrCloseDashboard => { HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || { 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; notify_event(app_handle, NotificationEvent::DashboardToggled).await;
}); });
} }
@@ -243,11 +243,11 @@ impl Hotkey {
); );
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned { if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = app_handle_cloned.get_webview_window("main") { if let Some(window) = app_handle_cloned.get_webview_window("main")
if window.is_focused().unwrap_or(false) { && window.is_focused().unwrap_or(false)
logging!(debug, Type::Hotkey, "Executing quit function"); {
Self::execute_function(function_owned, &app_handle_cloned); logging!(debug, Type::Hotkey, "Executing quit function");
} Self::execute_function(function_owned, &app_handle_cloned);
} }
} else { } else {
logging!(debug, Type::Hotkey, "Executing function directly"); 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 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 hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; 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) { const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service" r"\\.\pipe\clash-verge-service"
@@ -112,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
Ok(expected_signature == response.signature) Ok(expected_signature == response.signature)
} }
// IPC连接管理-win fn create_backoff_strategy() -> ExponentialBackoff {
#[cfg(target_os = "windows")] 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( pub async fn send_ipc_request(
command: IpcCommand, command: IpcCommand,
payload: serde_json::Value, payload: serde_json::Value,
) -> Result<IpcResponse> { ) -> 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 command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) { let operation = || async {
Ok(req) => req, match send_ipc_request_internal(command.clone(), payload.clone()).await {
Err(e) => { Ok(response) => Ok(response),
logging!(error, Type::Service, true, "创建签名请求失败: {}", e); Err(e) => {
return Err(e); logging!(
warn,
Type::Service,
true,
"IPC请求失败,准备重试: 命令={}, 错误={}",
command_type,
e
);
Err(BackoffError::transient(e))
}
} }
}; };
let request_json = serde_json::to_string(&request)?; match backoff::future::retry(create_backoff_strategy(), operation).await {
Ok(response) => {
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> { // logging!(
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) { // info,
Ok(name) => name, // Type::Service,
Err(e) => { // true,
logging!(error, Type::Service, true, "创建CString失败: {}", e); // "IPC请求成功: 命令={}, 成功={}",
return Err(anyhow::anyhow!("创建CString失败: {}", e)); // command_type,
} // response.success
}; // );
Ok(response)
let handle = unsafe { }
CreateFileA( Err(e) => {
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();
logging!( logging!(
error, error,
Type::Service, Type::Service,
true, true,
"连接到服务命名管道失败: {}", "IPC请求最终失败,重试已耗尽: 命令={}, 错误={}",
error command_type,
e
); );
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error)); Err(anyhow::anyhow!("IPC请求重试失败: {}", e))
} }
}
}
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) }; // 内部IPC请求实现(不带重试)
logging!(info, Type::Service, true, "服务连接成功 (Windows)"); 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(); // IPC连接管理-win
let len_bytes = (request_bytes.len() as u32).to_be_bytes(); #[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) { let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e); Ok(p) => p,
return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); Err(e) => {
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
} }
};
if let Err(e) = pipe.write_all(request_bytes) { logging!(info, Type::Service, true, "服务连接成功 (Windows)");
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
if let Err(e) = pipe.flush() { pipe.write_all(&len_bytes).await?;
logging!(error, Type::Service, true, "刷新管道失败: {}", e); pipe.write_all(request_bytes).await?;
return Err(anyhow::anyhow!("刷新管道失败: {}", e)); pipe.flush().await?;
}
let mut response_len_bytes = [0u8; 4]; let mut response_len_bytes = [0u8; 4];
if let Err(e) = pipe.read_exact(&mut response_len_bytes) { pipe.read_exact(&mut response_len_bytes).await?;
logging!(error, Type::Service, true, "读取响应长度失败: {}", e); let response_len = u32::from_be_bytes(response_len_bytes) as usize;
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
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]; let response: IpcResponse = serde_json::from_slice(&response_bytes)
if let Err(e) = pipe.read_exact(&mut response_bytes) { .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
}
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) { if !verify_response_signature(&response)? {
Ok(r) => r, logging!(error, Type::Service, true, "服务响应签名验证失败");
Err(e) => { bail!("服务响应签名验证失败");
logging!(error, Type::Service, true, "服务响应解析失败: {}", e); }
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) { Ok(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)
} }
// IPC连接管理-unix // IPC连接管理-unix
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub async fn send_ipc_request( async fn send_ipc_request_unix(
command: IpcCommand, command: IpcCommand,
payload: serde_json::Value, payload: serde_json::Value,
) -> Result<IpcResponse> { ) -> Result<IpcResponse> {
use std::os::unix::net::UnixStream; let request = create_signed_request(command, payload)?;
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_json = serde_json::to_string(&request)?; let request_json = serde_json::to_string(&request)?;
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) { let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await {
Ok(s) => { Ok(s) => s,
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
s
}
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e); logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务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 request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_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) { stream.write_all(&len_bytes).await?;
logging!(error, Type::Service, true, "写入请求长度失败: {}", e); stream.write_all(request_bytes).await?;
return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); stream.flush().await?;
}
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
// 读取响应长度
let mut response_len_bytes = [0u8; 4]; let mut response_len_bytes = [0u8; 4];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) { stream.read_exact(&mut response_len_bytes).await?;
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
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]; let mut response_bytes = vec![0u8; response_len];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) { stream.read_exact(&mut response_bytes).await?;
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))?;
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) 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::utils::autostart as startup_shortcut;
use crate::{ use crate::{
config::{Config, IVerge}, config::{Config, IVerge},
core::{handle::Handle, EventDrivenProxyManager}, core::{EventDrivenProxyManager, handle::Handle},
logging, logging_error, singleton_lazy, logging, logging_error, singleton_lazy,
utils::logging::Type, utils::logging::Type,
}; };
@@ -24,8 +24,7 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;
static DEFAULT_BYPASS: &str = 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"; "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")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = 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>";
"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 { async fn get_bypass() -> String {
let use_default = Config::verge() let use_default = Config::verge()
+15 -16
View File
@@ -6,8 +6,8 @@ use std::{
collections::HashMap, collections::HashMap,
pin::Pin, pin::Pin,
sync::{ sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
}, },
}; };
@@ -242,19 +242,18 @@ impl Timer {
if let Some(items) = Config::profiles().await.latest_ref().get_items() { if let Some(items) = Config::profiles().await.latest_ref().get_items() {
for item in items.iter() { for item in items.iter() {
if let Some(option) = item.option.as_ref() { if let Some(option) = item.option.as_ref()
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) { && let (Some(interval), Some(uid)) = (option.update_interval, &item.uid)
if interval > 0 { && interval > 0
logging!( {
debug, logging!(
Type::Timer, debug,
"找到定时更新配置: uid={}, interval={}min", Type::Timer,
uid, "找到定时更新配置: uid={}, interval={}min",
interval uid,
); interval
new_map.insert(uid.clone(), interval); );
} new_map.insert(uid.clone(), interval);
}
} }
} }
} }
@@ -390,7 +389,8 @@ impl Timer {
}; };
// Get the profile updated timestamp - now safe to await // 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() { let items = match profiles.get_items() {
Some(i) => i, Some(i) => i,
None => { None => {
@@ -434,7 +434,6 @@ impl Timer {
/// Emit update events for frontend notification /// Emit update events for frontend notification
fn emit_update_event(_uid: &str, _is_start: bool) { fn emit_update_event(_uid: &str, _is_start: bool) {
#[cfg(any(feature = "verge-dev", feature = "default"))]
{ {
if _is_start { if _is_start {
super::handle::Handle::notify_profile_update_started(_uid.to_string()); super::handle::Handle::notify_profile_update_started(_uid.to_string());
+39 -53
View File
@@ -1,12 +1,14 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use tauri::tray::TrayIconBuilder;
use tauri::Emitter; use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod speed_rate; pub mod speed_rate;
use crate::ipc::Rate; use crate::ipc::Rate;
use crate::module::lightweight;
use crate::process::AsyncHandler; use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
use crate::{ use crate::{
cmd, Type, cmd,
config::Config, config::Config,
feat, feat,
ipc::IpcManager, ipc::IpcManager,
@@ -14,7 +16,6 @@ use crate::{
module::lightweight::is_in_lightweight_mode, module::lightweight::is_in_lightweight_mode,
singleton_lazy, singleton_lazy,
utils::{dirs::find_target_icons, i18n::t}, utils::{dirs::find_target_icons, i18n::t},
Type,
}; };
use super::handle; use super::handle;
@@ -27,9 +28,9 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tauri::{ use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconEvent}, tray::{MouseButton, MouseButtonState, TrayIconEvent},
AppHandle, Wry,
}; };
#[derive(Clone)] #[derive(Clone)]
@@ -74,12 +75,11 @@ impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) { pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_ref().clone();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon { if is_common_tray_icon
if let Ok(Some(common_icon_path)) = find_target_icons("common") { && let Ok(Some(common_icon_path)) = find_target_icons("common")
if let Ok(icon_data) = fs::read(common_icon_path) { && let Ok(icon_data) = fs::read(common_icon_path)
return (true, icon_data); {
} return (true, icon_data);
}
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@@ -109,12 +109,11 @@ impl TrayState {
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) { pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_ref().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon { if is_sysproxy_tray_icon
if let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") { && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
if let Ok(icon_data) = fs::read(sysproxy_icon_path) { && let Ok(icon_data) = fs::read(sysproxy_icon_path)
return (true, icon_data); {
} return (true, icon_data);
}
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@@ -144,12 +143,11 @@ impl TrayState {
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) { pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_ref().clone();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon { if is_tun_tray_icon
if let Ok(Some(tun_icon_path)) = find_target_icons("tun") { && let Ok(Some(tun_icon_path)) = find_target_icons("tun")
if let Ok(icon_data) = fs::read(tun_icon_path) { && let Ok(icon_data) = fs::read(tun_icon_path)
return (true, icon_data); {
} return (true, icon_data);
}
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@@ -429,13 +427,13 @@ impl Tray {
{ {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_ref();
if let Some(current_profile_uid) = profiles.get_current() { if let Some(current_profile_uid) = profiles.get_current()
if let Ok(profile) = profiles.get_item(&current_profile_uid) { && let Ok(profile) = profiles.get_item(&current_profile_uid)
current_profile_name = match &profile.name { {
Some(profile_name) => profile_name.to_string(), current_profile_name = match &profile.name {
None => current_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; feat::toggle_tun_mode(None).await;
}), }),
"main_window" => Box::pin(async move { "main_window" => Box::pin(async move {
use crate::utils::window_manager::WindowManager; if !lightweight::exit_lightweight_mode().await {
log::info!(target: "app", "Tray点击事件: 显示主窗口"); WindowManager::show_main_window().await;
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:?}");
}), }),
_ => Box::pin(async move {}), _ => Box::pin(async move {}),
}; };
@@ -642,7 +635,9 @@ async fn create_tray_menu(
let should_show = match mode { let should_show = match mode {
"global" => group_name == "GLOBAL", "global" => group_name == "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 { if !should_show {
continue; continue;
@@ -971,19 +966,14 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into()).await; // Await async function feat::change_clash_mode(mode.into()).await; // Await async function
} }
"open_window" => { "open_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "托盘菜单点击: 打开窗口"); log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() { if !should_handle_tray_click() {
return; return;
} }
if !lightweight::exit_lightweight_mode().await {
if crate::module::lightweight::is_in_lightweight_mode() { WindowManager::show_main_window().await;
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:?}");
} }
"system_proxy" => { "system_proxy" => {
feat::toggle_system_proxy().await; // Await async function feat::toggle_system_proxy().await; // Await async function
@@ -1008,14 +998,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
return; return;
} }
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode(); if !is_in_lightweight_mode() {
if was_lightweight { lightweight::entry_lightweight_mode().await; // Await async function
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:?}");
} else { } else {
crate::module::lightweight::entry_lightweight_mode().await; // Remove .await as it's not async lightweight::exit_lightweight_mode().await; // Await async function
} }
} }
"quit" => { "quit" => {
+1 -1
View File
@@ -1,7 +1,7 @@
#![cfg(target_os = "windows")] #![cfg(target_os = "windows")]
use crate::utils::dirs; use crate::utils::dirs;
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::process::Command as StdCommand; 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)); 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"); log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new() Mapping::new()
}); })
config
} }
#[test] #[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() { if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml"); let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists() { if dns_path.exists()
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) { && let Ok(dns_yaml) = fs::read_to_string(&dns_path)
if let Ok(dns_config) = && let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml) {
{ // 处理hosts配置
// 处理hosts配置 if let Some(hosts_value) = dns_config.get("hosts")
if let Some(hosts_value) = dns_config.get("hosts") { && hosts_value.is_mapping()
if hosts_value.is_mapping() { {
config.insert("hosts".into(), hosts_value.clone()); config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration"); log::info!(target: "app", "apply hosts configuration");
} }
}
if let Some(dns_value) = dns_config.get("dns") { if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() { if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into()); config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)"); 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");
}
} }
} 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, config: Mapping,
name: String, name: String,
) -> Result<(Mapping, Vec<(String, 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}; use std::{cell::RefCell, rc::Rc};
let mut context = Context::default(); 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)); config.insert(Value::String(field.into()), Value::Sequence(new_seq));
// If this is proxies field, we also need to filter proxy-groups // If this is proxies field, we also need to filter proxy-groups
if field == "proxies" { if field == "proxies"
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") { && let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups")
let mut new_groups = Sequence::new(); {
for group in groups { let mut new_groups = Sequence::new();
if let Value::Mapping(group_map) = group { for group in groups {
let mut new_group = group_map.clone(); if let Value::Mapping(group_map) = group {
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") { let mut new_group = group_map.clone();
let filtered_proxies: Sequence = proxies if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
.iter() let filtered_proxies: Sequence = proxies
.filter(|p| { .iter()
if let Value::String(name) = p { .filter(|p| {
!delete.contains(name) if let Value::String(name) = p {
} else { !delete.contains(name)
true } else {
} true
}) }
.cloned() })
.collect(); .cloned()
new_group.insert( .collect();
Value::String("proxies".into()), new_group.insert(
Value::Sequence(filtered_proxies), Value::String("proxies".into()),
); Value::Sequence(filtered_proxies),
} );
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
} }
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 config
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, tray, CoreManager}, core::{CoreManager, handle, tray},
ipc::IpcManager, ipc::IpcManager,
logging_error, logging_error,
process::AsyncHandler, process::AsyncHandler,
+5 -5
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
config::{Config, IVerge}, config::{Config, IVerge},
core::{handle, hotkey, sysopt, tray, CoreManager}, core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error, logging_error,
module::lightweight, module::lightweight,
utils::logging::Type, 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 { if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?; sysopt::Sysopt::global().update_sysproxy().await?;
} }
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 { if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
if let Some(hotkeys) = patch.hotkeys { && let Some(hotkeys) = patch.hotkeys
hotkey::Hotkey::global().update(hotkeys).await?; {
} hotkey::Hotkey::global().update(hotkeys).await?;
} }
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 { if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu().await?; tray::Tray::global().update_menu().await?;
+3 -3
View File
@@ -1,11 +1,11 @@
use crate::{ use crate::{
cmd, cmd,
config::{profiles::profiles_draft_update_item_safe, Config, PrfItem, PrfOption}, config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{handle, tray, CoreManager}, core::{CoreManager, handle, tray},
logging, logging,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
/// Toggle proxy profile /// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) { 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 enable = {
let verge = Config::verge().await; 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 auto_close_connection = {
let verge = Config::verge().await; 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,则关闭所有连接 // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable && auto_close_connection { if enable
if let Err(err) = IpcManager::global().close_all_connections().await { && auto_close_connection
log::error!(target: "app", "Failed to close all connections: {err}"); && 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( let patch_result = super::patch_verge(
+11 -47
View File
@@ -1,57 +1,21 @@
use crate::utils::window_manager::WindowManager;
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, sysopt, CoreManager}, core::{CoreManager, handle, sysopt},
ipc::IpcManager, ipc::IpcManager,
logging, logging,
module::lightweight,
utils::logging::Type, utils::logging::Type,
}; };
/// Open or close the dashboard window /// Open or close the dashboard window
#[allow(dead_code)]
pub async fn open_or_close_dashboard() { pub async fn open_or_close_dashboard() {
open_or_close_dashboard_internal(false).await open_or_close_dashboard_internal().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
} }
/// Internal implementation for opening/closing dashboard /// Internal implementation for opening/closing dashboard
async fn open_or_close_dashboard_internal(bypass_debounce: bool) { async fn open_or_close_dashboard_internal() {
use crate::utils::window_manager::WindowManager; let _ = lightweight::exit_lightweight_mode().await;
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;
}
let result = WindowManager::toggle_main_window().await; let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Window toggle result: {result:?}"); log::info!(target: "app", "Window toggle result: {result:?}");
} }
@@ -92,7 +56,7 @@ pub async fn quit() {
} }
async fn clean_async() -> bool { async fn clean_async() -> bool {
use tokio::time::{timeout, Duration}; use tokio::time::{Duration, timeout};
logging!(info, Type::System, true, "开始执行异步清理操作..."); logging!(info, Type::System, true, "开始执行异步清理操作...");
@@ -252,10 +216,10 @@ pub async fn hide() {
add_light_weight_timer().await; add_light_weight_timer().await;
} }
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window()
if window.is_visible().unwrap_or(false) { && window.is_visible().unwrap_or(false)
let _ = window.hide(); {
} let _ = window.hide();
} }
handle::Handle::global().set_activation_policy_accessory(); handle::Handle::global().set_activation_policy_accessory();
} }
+9 -11
View File
@@ -1,10 +1,10 @@
use std::time::Duration; use std::time::Duration;
use kode_bridge::{ use kode_bridge::{
errors::{AnyError, AnyResult},
ClientConfig, IpcHttpClient, LegacyResponse, ClientConfig, IpcHttpClient, LegacyResponse,
errors::{AnyError, AnyResult},
}; };
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use crate::{ use crate::{
logging, singleton_with_logging, logging, singleton_with_logging,
@@ -30,7 +30,8 @@ pub struct IpcManager {
} }
impl 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| { let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path 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 { impl IpcManager {
pub async fn request( pub async fn request(
&self, &self,
@@ -199,8 +197,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递 // 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}"); let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await; self.send_request("GET", &url, None).await
response
} }
// 版本和配置相关 // 版本和配置相关
@@ -279,7 +276,6 @@ impl IpcManager {
let payload = serde_json::json!({ let payload = serde_json::json!({
"name": proxy "name": proxy
}); });
match self.send_request("PUT", &url, Some(&payload)).await { match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => { Err(e) => {
@@ -340,8 +336,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递 // 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}"); let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await; self.send_request("GET", &url, None).await
response
} }
// 调试相关 // 调试相关
@@ -370,3 +365,6 @@ impl IpcManager {
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理 // 日志相关功能已迁移到 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)] #![allow(non_snake_case)]
#![recursion_limit = "512"] #![recursion_limit = "512"]
mod cache;
mod cmd; mod cmd;
pub mod config; pub mod config;
mod core; mod core;
@@ -9,7 +10,6 @@ mod feat;
mod ipc; mod ipc;
mod module; mod module;
mod process; mod process;
mod state;
mod utils; mod utils;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
@@ -26,7 +26,7 @@ use tauri::Manager;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration}; use tokio::time::{Duration, timeout};
use utils::logging::Type; use utils::logging::Type;
/// Application initialization helper functions /// Application initialization helper functions
@@ -74,9 +74,12 @@ mod app_init {
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::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()); builder = builder.plugin(tauri_plugin_devtools::init());
} }
@@ -134,8 +137,8 @@ mod app_init {
} }
/// Generate all command handlers for the application /// Generate all command handlers for the application
pub fn generate_handlers( pub fn generate_handlers()
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static { -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![ tauri::generate_handler![
// Common commands // Common commands
cmd::get_sys_proxy, cmd::get_sys_proxy,
@@ -155,7 +158,6 @@ mod app_init {
// Application lifecycle // Application lifecycle
cmd::notify_ui_ready, cmd::notify_ui_ready,
cmd::update_ui_stage, cmd::update_ui_stage,
cmd::reset_ui_ready_state,
cmd::get_running_mode, cmd::get_running_mode,
cmd::get_app_uptime, cmd::get_app_uptime,
cmd::get_auto_launch_status, cmd::get_auto_launch_status,
@@ -178,6 +180,8 @@ mod app_init {
cmd::get_runtime_yaml, cmd::get_runtime_yaml,
cmd::get_runtime_exists, cmd::get_runtime_exists,
cmd::get_runtime_logs, cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool, cmd::invoke_uwp_tool,
cmd::copy_clash_env, cmd::copy_clash_env,
cmd::get_proxies, cmd::get_proxies,
@@ -269,13 +273,14 @@ pub fn run() {
// Setup singleton check // Setup singleton check
app_init::init_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 // Set Linux environment variable
#[cfg(target_os = "linux")] #[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") let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default() .unwrap_or_default()
@@ -284,7 +289,9 @@ pub fn run() {
let is_plasma_desktop = desktop_env.contains("PLASMA"); let is_plasma_desktop = desktop_env.contains("PLASMA");
if is_kde_desktop || is_plasma_desktop { if is_kde_desktop || is_plasma_desktop {
std::env::set_var("GTK_CSD", "0"); unsafe {
std::env::set_var("GTK_CSD", "0");
}
logging!( logging!(
info, info,
Type::Setup, Type::Setup,
@@ -438,10 +445,10 @@ pub fn run() {
} }
} }
if !is_enable_global_hotkey { if !is_enable_global_hotkey
if let Err(e) = hotkey::Hotkey::global().init().await { && let Err(e) = hotkey::Hotkey::global().init().await
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e); {
} logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
} }
return; return;
} }
@@ -474,10 +481,8 @@ pub fn run() {
} }
} }
if !is_enable_global_hotkey { if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() {
if let Err(e) = hotkey::Hotkey::global().reset() { logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
}
} }
}); });
} }
+133 -101
View File
@@ -1,38 +1,89 @@
use crate::{ use crate::{
cache::CacheProxy,
config::Config, config::Config,
core::{handle, timer::Timer, tray::Tray}, core::{handle, timer::Timer, tray::Tray},
log_err, logging, log_err, logging,
process::AsyncHandler, process::AsyncHandler,
state::proxy::ProxyRequestCache, utils::logging::Type,
utils::{logging::Type, window_manager::WindowManager},
}; };
#[cfg(target_os = "macos")]
use crate::logging_error; use crate::logging_error;
use crate::utils::window_manager::WindowManager;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder; use delay_timer::prelude::TaskBuilder;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
use tauri::{Listener, Manager}; use tauri::Listener;
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用 #[repr(u8)]
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LightweightState {
static IS_IN_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); Normal = 0,
In = 1,
fn inner_set_lightweight_mode(value: bool) -> bool { Exiting = 2,
if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
IS_IN_LIGHTWEIGHT.store(value, Ordering::SeqCst);
value
} }
fn inner_get_lightweight_mode() -> bool { impl From<u8> for LightweightState {
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst) || !WindowManager::is_main_window_exists() 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() { pub async fn run_once_auto_lightweight() {
@@ -56,18 +107,8 @@ pub async fn run_once_auto_lightweight() {
return; return;
} }
logging!( set_lightweight_mode(true).await;
info,
Type::Lightweight,
true,
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
enable_auto_light_weight_mode().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<()> { pub async fn auto_lightweight_mode_init() -> Result<()> {
@@ -88,29 +129,13 @@ pub async fn auto_lightweight_mode_init() -> Result<()> {
true, true,
"非静默启动直接挂载自动进入轻量模式监听器!" "非静默启动直接挂载自动进入轻量模式监听器!"
); );
set_lightweight_mode(true).await; set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await; enable_auto_light_weight_mode().await;
} }
Ok(()) 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() { pub async fn enable_auto_light_weight_mode() {
if let Err(e) = Timer::global().init().await { if let Err(e) = Timer::global().init().await {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}"); 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, "关闭自动轻量模式"); logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
cancel_window_close_listener(); cancel_window_close_listener();
cancel_webview_focus_listener();
} }
pub async fn entry_lightweight_mode() { pub async fn entry_lightweight_mode() -> bool {
use crate::utils::window_manager::WindowManager; // 尝试从 Normal -> In
if LIGHTWEIGHT_STATE
let result = WindowManager::hide_main_window(); .compare_exchange(
logging!( LightweightState::Normal as u8,
info, LightweightState::In as u8,
Type::Lightweight, Ordering::Acquire,
true, Ordering::Relaxed,
"轻量模式隐藏窗口结果: {:?}", )
result .is_err()
); {
logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用");
if let Some(window) = handle::Handle::global().get_window() { return false;
if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy();
}
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_accessory();
} }
WindowManager::destroy_main_window();
set_lightweight_mode(true).await; set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
ProxyRequestCache::global().clean_default_keys();
// 更新托盘显示 // 回到 In
logging_error!(Type::Lightweight, true, Tray::global().update_part().await); set_state(LightweightState::In);
CacheProxy::global().clean_default_keys();
true
} }
// 添加从轻量模式恢复的函数 // 添加从轻量模式恢复的函数
pub async fn exit_lightweight_mode() { pub async fn exit_lightweight_mode() -> bool {
// 使用原子操作检查是否已经在退出过程中,防止并发调用 // 尝试从 In -> Exiting
if EXITING_LIGHTWEIGHT if LIGHTWEIGHT_STATE
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .compare_exchange(
LightweightState::In as u8,
LightweightState::Exiting as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err() .is_err()
{ {
logging!( logging!(
info, info,
Type::Lightweight, Type::Lightweight,
true, true,
"轻量模式退出操作已在进行中,跳过重复调用" "轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
); );
return; return false;
} }
// 使用defer确保无论如何都会重置标志 WindowManager::show_main_window().await;
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
return;
}
set_lightweight_mode(false).await; set_lightweight_mode(false).await;
let _ = cancel_light_weight_timer();
// macOS激活策略 // 回到 Normal
#[cfg(target_os = "macos")] set_state(LightweightState::Normal);
handle::Handle::global().set_activation_policy_regular();
// 重置UI就绪状态 logging!(info, Type::Lightweight, true, "轻量模式退出完成");
crate::utils::resolve::ui::reset_ui_ready(); true
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -199,7 +218,7 @@ pub async fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer().await); 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() { if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://close-requested", move |_event| { let handler = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async { 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() { if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://focus", move |_event| { let handler = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer()); 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() { if let Some(window) = handle::Handle::global().get_window() {
window.unlisten(setup_window_close_listener()); let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听"); if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口焦点监听");
}
} }
} }
+8 -2
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
cmd::system, cmd::system,
core::{handle, CoreManager}, core::{CoreManager, handle},
}; };
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use sysinfo::System; use sysinfo::System;
@@ -20,7 +20,13 @@ impl Debug for PlatformSpecification {
write!( write!(
f, f,
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}", "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() location.column()
); );
println!("┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐"); println!(
"┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐"
);
println!("│ {:<18} │ {:<80} │", "Field", "Value"); println!("│ {:<18} │ {:<80} │", "Field", "Value");
println!("├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤"); println!(
"├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤"
);
println!("│ {:<18} │ {:<80} │", "Type of task", type_str); println!("│ {:<18} │ {:<80} │", "Type of task", type_str);
println!("│ {:<18} │ {:<80} │", "Size of task", size_str); println!("│ {:<18} │ {:<80} │", "Size of task", size_str);
println!("│ {:<18} │ {:<80} │", "Called from", loc_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")] #[cfg(target_os = "windows")]
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use log::info; 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 entry = entry?;
let path = entry.path(); let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
if file_name.starts_with(target) && file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png")) && (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{ {
matching_files.push(path); matching_files.push(path);
}
} }
} }
+3 -7
View File
@@ -1,7 +1,7 @@
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type}; 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 nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize}; use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr};
@@ -154,10 +154,6 @@ macro_rules! ret_err {
#[macro_export] #[macro_export]
macro_rules! t { macro_rules! t {
($en:expr, $zh:expr, $use_zh:expr) => { ($en:expr, $zh:expr, $use_zh:expr) => {
if $use_zh { if $use_zh { $zh } else { $en }
$zh
} else {
$en
}
}; };
} }
+16 -17
View File
@@ -15,14 +15,14 @@ fn get_locales_dir() -> Option<PathBuf> {
pub fn get_supported_languages() -> Vec<String> { pub fn get_supported_languages() -> Vec<String> {
let mut languages = Vec::new(); let mut languages = Vec::new();
if let Some(locales_dir) = get_locales_dir() { if let Some(locales_dir) = get_locales_dir()
if let Ok(entries) = fs::read_dir(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() { for entry in entries.flatten() {
if let Some(lang) = file_name.strip_suffix(".json") { if let Some(file_name) = entry.file_name().to_str()
languages.push(lang.to_string()); && 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() { if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() { for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{lang}.json")); let file_path = locales_dir.join(format!("{lang}.json"));
if let Ok(content) = fs::read_to_string(file_path) { if let Ok(content) = fs::read_to_string(file_path)
if let Ok(json) = serde_json::from_str(&content) { && let Ok(json) = serde_json::from_str(&content)
translations.insert(lang.to_string(), json); {
} translations.insert(lang.to_string(), json);
} }
} }
} }
@@ -76,14 +76,13 @@ pub async fn t(key: &str) -> String {
return text.to_string(); return text.to_string();
} }
if current_lang != DEFAULT_LANGUAGE { if current_lang != DEFAULT_LANGUAGE
if let Some(text) = TRANSLATIONS && let Some(text) = TRANSLATIONS
.get(DEFAULT_LANGUAGE) .get(DEFAULT_LANGUAGE)
.and_then(|trans| trans.get(&key)) .and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str()) .and_then(|val| val.as_str())
{ {
return text.to_string(); return text.to_string();
}
} }
key 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::{ use crate::{
config::*, config::*,
core::handle, core::handle,
@@ -7,64 +15,47 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use chrono::{Local, TimeZone}; 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 std::{path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tokio::fs; use tokio::fs;
use tokio::fs::DirEntry; use tokio::fs::DirEntry;
/// initialize this instance's log file /// initialize this instance's log file
async fn init_log() -> Result<()> { #[cfg(not(feature = "tauri-dev"))]
let log_dir = dirs::app_logs_dir()?; pub async fn init_logger() -> Result<()> {
if !log_dir.exists() {
let _ = tokio::fs::create_dir_all(&log_dir).await;
}
let log_level = Config::verge().await.latest_ref().get_log_level(); let log_level = Config::verge().await.latest_ref().get_log_level();
if log_level == LevelFilter::Off { 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_dir = dirs::app_logs_dir()?;
let log_file = format!("{local_time}.log"); let logger = Logger::with(LogSpecification::from(log_level))
let log_file = log_dir.join(log_file); .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 { let _handle = logger.start()?;
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
};
let encode = Box::new(PatternEncoder::new(log_pattern)); // TODO 全局 logger handle 控制
// GlobalLoggerProxy::global().set_inner(handle);
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build(); // TODO 提供前端设置等级,热更新等级
let tofile = FileAppender::builder().encoder(encode).build(log_file)?; // logger.parse_new_spec(spec)
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)?;
Ok(()) Ok(())
} }
// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件
/// 删除log文件 /// 删除log文件
pub async fn delete_log() -> Result<()> { pub async fn delete_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?; let log_dir = dirs::app_logs_dir()?;
@@ -296,52 +287,52 @@ async fn ensure_directories() -> Result<()> {
/// 初始化配置文件 /// 初始化配置文件
async fn initialize_config_files() -> Result<()> { async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::clash_path() { if let Ok(path) = dirs::clash_path()
if !path.exists() { && !path.exists()
let template = IClashTemp::template().0; {
help::save_yaml(&path, &template, Some("# Clash Verge")) let template = IClashTemp::template().0;
.await help::save_yaml(&path, &template, Some("# Clash Verge"))
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?; .await
logging!( .map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
info, logging!(
Type::Setup, info,
true, Type::Setup,
"Created clash config at {:?}", true,
path "Created clash config at {:?}",
); path
} );
} }
if let Ok(path) = dirs::verge_path() { if let Ok(path) = dirs::verge_path()
if !path.exists() { && !path.exists()
let template = IVerge::template(); {
help::save_yaml(&path, &template, Some("# Clash Verge")) let template = IVerge::template();
.await help::save_yaml(&path, &template, Some("# Clash Verge"))
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?; .await
logging!( .map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
info, logging!(
Type::Setup, info,
true, Type::Setup,
"Created verge config at {:?}", true,
path "Created verge config at {:?}",
); path
} );
} }
if let Ok(path) = dirs::profiles_path() { if let Ok(path) = dirs::profiles_path()
if !path.exists() { && !path.exists()
let template = IProfiles::template(); {
help::save_yaml(&path, &template, Some("# Clash Verge")) let template = IProfiles::template();
.await help::save_yaml(&path, &template, Some("# Clash Verge"))
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?; .await
logging!( .map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
info, logging!(
Type::Setup, info,
true, Type::Setup,
"Created profiles config at {:?}", true,
path "Created profiles config at {:?}",
); path
} );
} }
// 验证并修正verge配置 // 验证并修正verge配置
@@ -355,11 +346,13 @@ async fn initialize_config_files() -> Result<()> {
/// Initialize all the config files /// Initialize all the config files
/// before tauri setup /// before tauri setup
pub async fn init_config() -> Result<()> { 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 { // We do not need init_log here anymore due to resolve will to the things
eprintln!("Failed to initialize logging: {}", e); // if let Err(e) = init_log().await {
} // eprintln!("Failed to initialize logging: {}", e);
// }
ensure_directories().await?; ensure_directories().await?;
@@ -459,7 +452,7 @@ pub async fn init_resources() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> { pub fn init_scheme() -> Result<()> {
use tauri::utils::platform::current_exe; use tauri::utils::platform::current_exe;
use winreg::{enums::*, RegKey}; use winreg::{RegKey, enums::*};
let app_exe = current_exe()?; let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type { pub enum Type {
@@ -107,7 +117,8 @@ macro_rules! wrap_err {
macro_rules! logging { macro_rules! logging {
// 带 println 的版本(支持格式化参数) // 带 println 的版本(支持格式化参数)
($level:ident, $type:expr, true, $($arg:tt)*) => { ($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)*)); log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
}; };
@@ -157,3 +168,56 @@ macro_rules! logging_error {
logging_error!($type, false, $fmt $(, $arg)*); 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 anyhow::Result;
use base64::{engine::general_purpose, Engine as _}; use base64::{Engine as _, engine::general_purpose};
use isahc::prelude::*; use isahc::prelude::*;
use isahc::{HttpClient, config::SslOption};
use isahc::{ use isahc::{
config::RedirectPolicy, config::RedirectPolicy,
http::{ http::{
header::{HeaderMap, HeaderValue, USER_AGENT},
StatusCode, Uri, StatusCode, Uri,
header::{HeaderMap, HeaderValue, USER_AGENT},
}, },
}; };
use isahc::{config::SslOption, HttpClient};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use sysproxy::Sysproxy; use sysproxy::Sysproxy;
use tauri::Url; use tauri::Url;
@@ -88,10 +88,11 @@ impl NetworkManager {
return true; return true;
} }
if let Some((time, _)) = &*last_error_guard { if let Some((time, _)) = &*last_error_guard
if time.elapsed() < Duration::from_secs(30) && count > 2 { && time.elapsed() < Duration::from_secs(30)
return true; && count > 2
} {
return true;
} }
false false
@@ -113,7 +114,8 @@ impl NetworkManager {
) -> Result<HttpClient> { ) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone(); let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone(); let headers_clone = default_headers.clone();
let client = {
{
let mut builder = HttpClient::builder(); let mut builder = HttpClient::builder();
builder = match proxy_uri_clone { builder = match proxy_uri_clone {
@@ -136,9 +138,7 @@ impl NetworkManager {
builder = builder.redirect_policy(RedirectPolicy::Follow); builder = builder.redirect_policy(RedirectPolicy::Follow);
Ok(builder.build()?) Ok(builder.build()?)
}; }
client
} }
pub async fn create_request( pub async fn create_request(
@@ -200,15 +200,15 @@ impl NetworkManager {
let parsed = Url::parse(url)?; let parsed = Url::parse(url)?;
let mut extra_headers = HeaderMap::new(); let mut extra_headers = HeaderMap::new();
if !parsed.username().is_empty() { if !parsed.username().is_empty()
if let Some(pass) = parsed.password() { && let Some(pass) = parsed.password()
let auth_str = format!("{}:{}", parsed.username(), pass); {
let encoded = general_purpose::STANDARD.encode(auth_str); let auth_str = format!("{}:{}", parsed.username(), pass);
extra_headers.insert( let encoded = general_purpose::STANDARD.encode(auth_str);
"Authorization", extra_headers.insert(
HeaderValue::from_str(&format!("Basic {}", encoded))?, "Authorization",
); HeaderValue::from_str(&format!("Basic {}", encoded))?,
} );
} }
let clean_url = { let clean_url = {
+37 -4
View File
@@ -3,11 +3,13 @@ use tauri::AppHandle;
use crate::{ use crate::{
config::Config, 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, logging, logging_error,
module::lightweight::auto_lightweight_mode_init, module::lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
process::AsyncHandler, process::AsyncHandler,
utils::{init, logging::Type, resolve::window::create_window, server}, utils::{init, logging::Type, server, window_manager::WindowManager},
}; };
pub mod dns; pub mod dns;
@@ -38,6 +40,10 @@ pub fn resolve_setup_async() {
); );
AsyncHandler::spawn(|| async { AsyncHandler::spawn(|| async {
#[cfg(not(feature = "tauri-dev"))]
resolve_setup_logger().await;
init_service_manager().await;
futures::join!( futures::join!(
init_work_config(), init_work_config(),
init_resources(), init_resources(),
@@ -46,6 +52,7 @@ pub fn resolve_setup_async() {
); );
init_timer().await; init_timer().await;
init_once_auto_lightweight().await;
init_auto_lightweight_mode().await; init_auto_lightweight_mode().await;
init_verge_config().await; init_verge_config().await;
@@ -114,6 +121,12 @@ pub(super) fn init_scheme() {
logging_error!(Type::Setup, true, init::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<()> { pub async fn resolve_scheme(param: String) -> Result<()> {
logging!( logging!(
info, info,
@@ -150,6 +163,16 @@ pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, true, Hotkey::global().init().await); 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() { pub(super) async fn init_auto_lightweight_mode() {
logging!( logging!(
info, info,
@@ -185,6 +208,15 @@ pub(super) async fn init_verge_config() {
logging_error!(Type::Setup, true, Config::init_config().await); 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() { pub(super) async fn init_core_manager() {
logging!(info, Type::Setup, true, "Initializing core manager..."); logging!(info, Type::Setup, true, "Initializing core manager...");
logging_error!(Type::Setup, true, CoreManager::global().init().await); 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() { pub(super) async fn init_window() {
logging!(info, Type::Setup, true, "Initializing main window...");
let is_silent_start = let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false); { Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -229,5 +262,5 @@ pub(super) async fn init_window() {
Handle::global().set_activation_policy_accessory(); 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 percent_encoding::percent_decode_str;
use tauri::Url; use tauri::Url;
@@ -44,7 +44,6 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
match url_param { match url_param {
Some(url) => { Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}"); log::info!(target:"app", "decoded subscription url: {url}");
// create_window(false).await;
match PrfItem::from_url(url.as_ref(), name, None, None).await { match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => { Ok(item) => {
let uid = match item.uid.clone() { let uid = match item.uid.clone() {
+1 -34
View File
@@ -1,8 +1,8 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Arc,
atomic::{AtomicBool, Ordering},
}; };
use tokio::sync::Notify; 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_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准备阶段 // 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) { pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state(); let state = get_ui_ready_state();
@@ -91,16 +71,3 @@ pub fn mark_ui_ready() {
// 通知所有等待的任务 // 通知所有等待的任务
get_ui_ready_notify().notify_waiters(); 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 tauri::WebviewWindow;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use tauri::{Manager, WebviewWindow};
use crate::{ use crate::{
core::handle, core::handle,
logging, logging, logging_error,
module::lightweight,
process::AsyncHandler,
utils::{ utils::{
logging::Type, logging::Type,
resolve::{ resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
ui::{get_ui_ready, update_ui_ready_stage, wait_for_ui_ready, UiReadyStage},
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_WIDTH: f64 = 520.0;
const MINIMAL_HEIGHT: 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 窗口 /// 构建新的 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(|| { let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
logging!( logging!(
error, error,
@@ -97,7 +28,7 @@ fn build_new_window() -> Result<WebviewWindow, String> {
"无法获取app_handle".to_string() "无法获取app_handle".to_string()
})?; })?;
tauri::WebviewWindowBuilder::new( match tauri::WebviewWindowBuilder::new(
&app_handle, &app_handle,
"main", /* the unique window label */ "main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
@@ -111,188 +42,11 @@ fn build_new_window() -> Result<WebviewWindow, String> {
.visible(true) // 立即显示窗口,避免用户等待 .visible(true) // 立即显示窗口,避免用户等待
.initialization_script(WINDOW_INITIAL_SCRIPT) .initialization_script(WINDOW_INITIAL_SCRIPT)
.build() .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) => { Ok(window) => {
// 窗口创建成功,重置锁状态 logging_error!(Type::Window, true, window.eval(INITIAL_LOADING_OVERLAY));
reset_window_creation_lock(); Ok(window)
window
} }
Err(_) => { Err(e) => Err(e.to_string()),
// 窗口创建失败,重置锁状态 }
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
} }
+2 -2
View File
@@ -1,11 +1,11 @@
use super::resolve; use super::resolve;
use crate::{ use crate::{
config::{Config, IVerge, DEFAULT_PAC}, config::{Config, DEFAULT_PAC, IVerge},
logging_error, logging_error,
process::AsyncHandler, process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use port_scanner::local_port_available; use port_scanner::local_port_available;
use warp::Filter; 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 tauri::{Manager, WebviewWindow, Wry};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@@ -18,6 +24,8 @@ pub enum WindowOperationResult {
Hidden, Hidden,
/// 创建了新窗口 /// 创建了新窗口
Created, Created,
/// 摧毁了窗口
Destroyed,
/// 操作失败 /// 操作失败
Failed, Failed,
/// 无需操作 /// 无需操作
@@ -138,7 +146,7 @@ impl WindowManager {
match current_state { match current_state {
WindowState::NotExist => { WindowState::NotExist => {
logging!(info, Type::Window, true, "窗口不存在,创建新窗口"); logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window().await { if Self::create_window(true).await {
logging!(info, Type::Window, true, "窗口创建成功"); logging!(info, Type::Window, true, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created WindowOperationResult::Created
@@ -198,7 +206,7 @@ impl WindowManager {
// 窗口不存在,创建新窗口 // 窗口不存在,创建新窗口
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口"); logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
// 由于已经有防抖保护,直接调用内部方法 // 由于已经有防抖保护,直接调用内部方法
if Self::create_new_window().await { if Self::create_window(true).await {
WindowOperationResult::Created WindowOperationResult::Created
} else { } else {
WindowOperationResult::Failed 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 { pub fn is_main_window_visible() -> bool {
Self::get_main_window() Self::get_main_window()
@@ -365,10 +346,54 @@ impl WindowManager {
} }
/// 创建新窗口,防抖避免重复调用 /// 创建新窗口,防抖避免重复调用
async fn create_new_window() -> bool { pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
use crate::utils::resolve; 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", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,
@@ -33,10 +33,7 @@
"endpoints": [ "endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "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://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://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"
], ],
"windows": { "windows": {
"installMode": "basicUi" "installMode": "basicUi"
+1 -3
View File
@@ -1,9 +1,7 @@
import { AppDataProvider } from "./providers/app-data-provider";
import Layout from "./pages/_layout"; import Layout from "./pages/_layout";
import { useNotificationPermission } from "./hooks/useNotificationPermission"; import { AppDataProvider } from "./providers/app-data-provider";
function App() { function App() {
useNotificationPermission();
return ( return (
<AppDataProvider> <AppDataProvider>
<Layout /> <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 { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import { import {
subscribeNotices, subscribeNotices,
hideNotice, hideNotice,
+3 -2
View File
@@ -1,4 +1,5 @@
import { ReactNode } from "react"; /* eslint-disable react/prop-types */
import { LoadingButton } from "@mui/lab";
import { import {
Button, Button,
Dialog, Dialog,
@@ -8,7 +9,7 @@ import {
type SxProps, type SxProps,
type Theme, type Theme,
} from "@mui/material"; } from "@mui/material";
import { LoadingButton } from "@mui/lab"; import { ReactNode } from "react";
interface Props { interface Props {
title: ReactNode; title: ReactNode;
+1 -1
View File
@@ -1,5 +1,5 @@
import { alpha, Box, Typography } from "@mui/material";
import { InboxRounded } from "@mui/icons-material"; import { InboxRounded } from "@mui/icons-material";
import { alpha, Box, Typography } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface Props { interface Props {
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
import { Box, styled } from "@mui/material"; import { Box, styled } from "@mui/material";
import React from "react";
type Props = { type Props = {
label: string; label: string;
+2 -4
View File
@@ -1,7 +1,7 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material"; import { Box, CircularProgress } from "@mui/material";
import React from "react";
export interface BaseLoadingOverlayProps { interface BaseLoadingOverlayProps {
isLoading: boolean; isLoading: boolean;
} }
@@ -29,5 +29,3 @@ export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
</Box> </Box>
); );
}; };
export default BaseLoadingOverlay;
+3 -2
View File
@@ -1,7 +1,8 @@
import React, { ReactNode } from "react";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import React, { ReactNode } from "react";
import { BaseErrorBoundary } from "./base-error-boundary";
interface Props { interface Props {
title?: React.ReactNode; // the page title title?: React.ReactNode; // the page title
+4 -3
View File
@@ -1,10 +1,11 @@
import { Box, SvgIcon, TextField, styled } from "@mui/material"; import { Box, SvgIcon, TextField, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.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 useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
import { useTranslation } from "react-i18next";
export type SearchState = { export type SearchState = {
text: string; text: string;
@@ -81,8 +82,8 @@ export const BaseSearchBox = (props: SearchProps) => {
return (content: string) => { return (content: string) => {
if (!searchText) return true; if (!searchText) return true;
let item = !matchCase ? content.toLowerCase() : content; const item = !matchCase ? content.toLowerCase() : content;
let searchItem = !matchCase ? searchText.toLowerCase() : searchText; const searchItem = !matchCase ? searchText.toLowerCase() : searchText;
if (useRegularExpression) { if (useRegularExpression) {
return new RegExp(searchItem).test(item); return new RegExp(searchItem).test(item);
+1 -1
View File
@@ -1,10 +1,10 @@
import { InfoRounded } from "@mui/icons-material";
import { import {
Tooltip, Tooltip,
IconButton, IconButton,
IconButtonProps, IconButtonProps,
SvgIconProps, SvgIconProps,
} from "@mui/material"; } from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
interface Props extends IconButtonProps { interface Props extends IconButtonProps {
title?: string; title?: string;
@@ -1,10 +1,10 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
import { Box, Typography, Button, Alert, Collapse } from "@mui/material";
import { import {
ErrorOutlineRounded, ErrorOutlineRounded,
RefreshRounded, RefreshRounded,
BugReportRounded, BugReportRounded,
} from "@mui/icons-material"; } 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"; import { useTranslation } from "react-i18next";
interface Props { 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 { 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 { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
export interface ConnectionDetailRef { export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void; open: (detail: IConnectionsItem) => void;
@@ -1,5 +1,4 @@
import dayjs from "dayjs"; import { CloseRounded } from "@mui/icons-material";
import { useLockFn } from "ahooks";
import { import {
styled, styled,
ListItem, ListItem,
@@ -8,7 +7,9 @@ import {
Box, Box,
alpha, alpha,
} from "@mui/material"; } from "@mui/material";
import { CloseRounded } from "@mui/icons-material"; import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { deleteConnection } from "@/services/cmds"; import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic"; 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 { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
import { truncateStr } from "@/utils/truncate-str"; import dayjs from "dayjs";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
import { useLocalStorage } from "foxact/use-local-storage"; 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 { interface Props {
connections: IConnectionsItem[]; 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 { DeveloperBoardOutlined } from "@mui/icons-material";
import { useClash } from "@/hooks/use-clash"; import { Typography, Stack, Divider } from "@mui/material";
import { EnhancedCard } from "./enhanced-card";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useClash } from "@/hooks/use-clash";
import { useAppData } from "@/providers/app-data-provider"; import { useAppData } from "@/providers/app-data-provider";
import { EnhancedCard } from "./enhanced-card";
// 将毫秒转换为时:分:秒格式的函数 // 将毫秒转换为时:分:秒格式的函数
const formatUptime = (uptimeMs: number) => { const formatUptime = (uptimeMs: number) => {
const hours = Math.floor(uptimeMs / 3600000); 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 { import {
LanguageRounded, LanguageRounded,
MultipleStopRounded, MultipleStopRounded,
DirectionsRounded, DirectionsRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { useLockFn } from "ahooks";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider"; import { useAppData } from "@/providers/app-data-provider";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
export const ClashModeCard = () => { export const ClashModeCard = () => {
const { t } = useTranslation(); 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 { import {
Box, Box,
Typography, Typography,
@@ -15,23 +26,13 @@ import {
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react"; import { useEffect, useState, useMemo, useCallback } from "react";
import { import { useTranslation } from "react-i18next";
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 { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card"; 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 { 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"; 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) { function convertDelayColor(delayValue: number) {
const colorStr = delayManager.formatDelayColor(delayValue); const colorStr = delayManager.formatDelayColor(delayValue);
@@ -1,3 +1,4 @@
import { Box, useTheme } from "@mui/material";
import { import {
forwardRef, forwardRef,
useImperativeHandle, useImperativeHandle,
@@ -8,16 +9,16 @@ import {
useRef, useRef,
memo, memo,
} from "react"; } from "react";
import { Box, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import parseTraffic from "@/utils/parse-traffic";
import { import {
useTrafficGraphDataEnhanced, useTrafficGraphDataEnhanced,
type ITrafficDataPoint, type ITrafficDataPoint,
} from "@/hooks/use-traffic-monitor"; } from "@/hooks/use-traffic-monitor";
import parseTraffic from "@/utils/parse-traffic";
// 流量数据项接口 // 流量数据项接口
export interface ITrafficItem { interface ITrafficItem {
up: number; up: number;
down: number; down: number;
timestamp?: number; timestamp?: number;
+1 -1
View File
@@ -2,7 +2,7 @@ import { Box, Typography, alpha, useTheme } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
// 自定义卡片组件接口 // 自定义卡片组件接口
export interface EnhancedCardProps { interface EnhancedCardProps {
title: ReactNode; title: ReactNode;
icon: ReactNode; icon: ReactNode;
action?: 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 { import {
ArrowUpwardRounded, ArrowUpwardRounded,
ArrowDownwardRounded, ArrowDownwardRounded,
@@ -17,19 +6,32 @@ import {
CloudUploadRounded, CloudUploadRounded,
CloudDownloadRounded, CloudDownloadRounded,
} from "@mui/icons-material"; } 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 { import {
EnhancedCanvasTrafficGraph, EnhancedCanvasTrafficGraph,
type EnhancedCanvasTrafficGraphRef, type EnhancedCanvasTrafficGraphRef,
} from "./enhanced-canvas-traffic-graph"; } 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 { interface StatCardProps {
icon: ReactNode; 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 { import {
Box, Box,
Typography, Typography,
@@ -10,24 +18,18 @@ import {
Link, Link,
keyframes, keyframes,
} from "@mui/material"; } 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 { useLockFn } from "ahooks";
import { showNotice } from "@/services/noticeService"; import dayjs from "dayjs";
import { EnhancedCard } from "./enhanced-card"; 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 { 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` const round = keyframes`
@@ -55,7 +57,7 @@ interface ProfileExtra {
expire: number; expire: number;
} }
export interface ProfileItem { interface ProfileItem {
uid: string; uid: string;
type?: "local" | "remote" | "merge" | "script"; type?: "local" | "remote" | "merge" | "script";
name?: string; name?: string;
@@ -68,7 +70,7 @@ export interface ProfileItem {
option?: any; option?: any;
} }
export interface HomeProfileCardProps { interface HomeProfileCardProps {
current: ProfileItem | null | undefined; current: ProfileItem | null | undefined;
onProfileUpdated?: () => void; 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 { import {
LocationOnOutlined, LocationOnOutlined,
RefreshOutlined, RefreshOutlined,
VisibilityOutlined, VisibilityOutlined,
VisibilityOffOutlined, VisibilityOffOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { EnhancedCard } from "./enhanced-card"; import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material";
import { getIpInfo } from "@/services/api";
import { useState, useEffect, useCallback, memo } from "react"; 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; 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 { import {
Box, Box,
Typography, Typography,
@@ -10,16 +15,12 @@ import {
Fade, Fade,
} from "@mui/material"; } from "@mui/material";
import { useState, useMemo, memo, FC } from "react"; import { useState, useMemo, memo, FC } from "react";
import { useTranslation } from "react-i18next";
import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches"; 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 { 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"; import { showNotice } from "@/services/noticeService";
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab"; 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 { import {
InfoOutlined, InfoOutlined,
SettingsOutlined, SettingsOutlined,
@@ -15,18 +6,29 @@ import {
DnsOutlined, DnsOutlined,
ExtensionOutlined, ExtensionOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge"; import {
import { EnhancedCard } from "./enhanced-card"; Typography,
import useSWR from "swr"; Stack,
import { getSystemInfo } from "@/services/cmds"; Divider,
import { useNavigate } from "react-router-dom"; Chip,
import { version as appVersion } from "@root/package.json"; IconButton,
import { useCallback, useEffect, useMemo, useState } from "react"; Tooltip,
} from "@mui/material";
import { check as checkUpdate } from "@tauri-apps/plugin-updater"; import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useLockFn } from "ahooks"; 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 { useSystemState } from "@/hooks/use-system-state";
import { useVerge } from "@/hooks/use-verge";
import { useServiceInstaller } from "@/hooks/useServiceInstaller"; 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 = () => { export const SystemInfoCard = () => {
const { t } = useTranslation(); 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 { import {
DndContext, DndContext,
closestCenter, closestCenter,
@@ -10,20 +7,23 @@ import {
DragEndEvent, DragEndEvent,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable"; import { SortableContext } from "@dnd-kit/sortable";
import { Add, NetworkCheck } from "@mui/icons-material";
import { useTranslation } from "react-i18next"; import { Box, IconButton, Tooltip, alpha, styled, Grid } from "@mui/material";
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
import { TestItem } from "@/components/test/test-item";
import { emit } from "@tauri-apps/api/event"; import { emit } from "@tauri-apps/api/event";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { Add, NetworkCheck } from "@mui/icons-material"; import { useEffect, useRef, useMemo, useCallback } from "react";
import { EnhancedCard } from "./enhanced-card"; import { useTranslation } from "react-i18next";
// test icons // test icons
import apple from "@/assets/image/test/apple.svg?raw"; import apple from "@/assets/image/test/apple.svg?raw";
import github from "@/assets/image/test/github.svg?raw"; import github from "@/assets/image/test/github.svg?raw";
import google from "@/assets/image/test/google.svg?raw"; import google from "@/assets/image/test/google.svg?raw";
import youtube from "@/assets/image/test/youtube.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 }) => ({ const ScrollBox = styled(Box)(({ theme }) => ({
+1
View File
@@ -6,6 +6,7 @@ import {
ListItemIcon, ListItemIcon,
} from "@mui/material"; } from "@mui/material";
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom"; import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
interface Props { interface Props {
to: string; to: string;

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