Compare commits

..
535 changed files with 31603 additions and 54035 deletions
-4
View File
@@ -3,7 +3,3 @@ linker = "aarch64-linux-gnu-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[alias]
clippy-all = "clippy --all-targets --all-features -- -D warnings"
clippy-only = "clippy --all-targets --features clippy -- -D warnings"
+6 -6
View File
@@ -13,8 +13,8 @@ body:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 查看 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本更新日志
5. 请 **务必** 尝试 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本,确定问题是否仍然存在
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
@@ -22,8 +22,8 @@ body:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to check out [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) version update log
5. Please be sure to try the [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) version to ensure that the problem still exists
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
- type: textarea
@@ -35,8 +35,8 @@ body:
required: true
- type: textarea
attributes:
label: 软件版本 / CVR Version
description: 请提供 CVR 的具体版本,如果是 AutoBuild 版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of CVR. If it is an AutoBuild version, please indicate the download time (accurate to hours and minutes)
label: 软件版本 / Verge Version
description: 请提供Verge的具体版本,如果是alpha版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of Verge. If it is an alpha version, please indicate the download time (accurate to hours and minutes)
render: text
validations:
required: true
+2 -2
View File
@@ -12,13 +12,13 @@ body:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to download the [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) version for testing to ensure that the function has not been implemented
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the function has not been implemented
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
- type: textarea
+2 -2
View File
@@ -52,7 +52,7 @@ body:
- type: input
id: verge-version
attributes:
label: 软件版本 / CVR Version
description: 请提供你使用的 CVR 具体版本 / Please provide the specific version of CVR you are using
label: 软件版本 / Verge Version
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
validations:
required: true
+18 -19
View File
@@ -25,7 +25,6 @@ env:
TAG_CHANNEL: Alpha
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
@@ -35,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Check tag and package.json version
id: check_tag
@@ -64,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Delete Old Alpha Tags Except Latest
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -180,23 +179,23 @@ jobs:
needs: delete_old_assets
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "Changelog.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md)
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in Changelog.md"
echo "No update sections found in UPDATELOG.md"
fi
else
echo "Changelog.md file not found"
echo "UPDATELOG.md file not found"
fi
shell: bash
@@ -275,7 +274,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
@@ -305,9 +304,9 @@ jobs:
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -360,7 +359,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
@@ -375,9 +374,9 @@ jobs:
save-if: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -491,7 +490,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -503,9 +502,9 @@ jobs:
save-if: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -522,8 +521,8 @@ jobs:
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 2
+134 -126
View File
@@ -9,9 +9,10 @@ permissions: write-all
env:
TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild
PRE_TAG_NAME: pre-autobuild
PRE_TAG_CHANNEL: PreAutoBuild
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -31,22 +32,35 @@ jobs:
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: bash ./scripts/extract_update_logs.sh
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- uses: pnpm/action-setup@v4.2.0
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -59,7 +73,7 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/${{ env.PRE_TAG_NAME }}" >> $GITHUB_ENV
shell: bash
- run: |
@@ -90,7 +104,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -101,15 +115,15 @@ jobs:
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: false
generate_release_notes: true
clean_old_assets:
name: Clean Old Release Assets
@@ -118,7 +132,7 @@ jobs:
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with:
tag_name: autobuild
tag_name: pre-autobuild
dry_run: false
autobuild-x86-windows-macos-linux:
@@ -142,13 +156,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -156,13 +167,10 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
workspaces: src-tauri
cache-all-crates: true
cache-workspace-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -179,40 +187,25 @@ jobs:
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- uses: pnpm/action-setup@v4.2.0
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
- name: Pnpm install and check
run: |
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build for Windows-macOS-Linux
uses: tauri-apps/tauri-action@v0
env:
@@ -227,14 +220,13 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: ${{ env.TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tagName: ${{ env.PRE_TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
autobuild-arm-linux:
name: Autobuild ARM Linux
@@ -253,13 +245,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -267,39 +256,28 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
workspaces: src-tauri
cache-all-crates: true
cache-workspace-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
- name: Pnpm install and check
run: |
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Setup for linux
@@ -351,13 +329,6 @@ jobs:
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -381,16 +352,16 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: |
target/${{ matrix.target }}/release/bundle/deb/*.deb
target/${{ matrix.target }}/release/bundle/rpm/*.rpm
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
autobuild-x86-arm-windows_webview2:
name: Autobuild x86 and ARM Windows with WebView2
@@ -409,7 +380,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -417,55 +388,37 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
workspaces: src-tauri
cache-all-crates: true
cache-workspace-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
- name: Pnpm install and check
run: |
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build for Windows
id: build
uses: tauri-apps/tauri-action@v0
@@ -477,41 +430,82 @@ jobs:
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
- name: Rename
run: |
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName
}
- name: Upload Release
- name: Upload Pre-Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.PRE_TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
promote-autobuild:
name: Promote PreAutoBuild to AutoBuild
runs-on: ubuntu-latest
needs:
[
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
if: >
${{
needs.autobuild-x86-windows-macos-linux.result == 'success' &&
needs.autobuild-arm-linux.result == 'success' &&
needs.autobuild-x86-arm-windows_webview2.result == 'success'
}}
steps:
- name: Install gh CLI
run: |
sudo apt-get update
sudo apt-get install -y gh jq
- name: Download PreAutoBuild Release Assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir pre-assets
release_id=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.PRE_TAG_NAME }} | jq .id)
gh release download ${{ env.PRE_TAG_NAME }} --dir pre-assets
body=$(gh api repos/${{ github.repository }}/releases/$release_id | jq -r .body)
echo "$body" > release.txt
- name: Publish to autobuild tag
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
files: pre-assets/*
notify-telegram:
name: Notify Telegram
@@ -522,22 +516,36 @@ jobs:
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
promote-autobuild,
]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: bash ./scripts/extract_update_logs.sh
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4.2.0
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
@@ -585,7 +593,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -53,7 +53,7 @@ jobs:
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 50
+3 -3
View File
@@ -42,7 +42,7 @@ jobs:
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 50
@@ -56,7 +56,7 @@ jobs:
echo "🔍 Finding last commit with Tauri-related changes..."
# Define patterns for Tauri-related files
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
# Get the last commit that changed any of these patterns (excluding build artifacts)
LAST_TAURI_COMMIT=""
@@ -105,7 +105,7 @@ jobs:
needs: check_current_version
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Clean old assets from release
env:
+3 -6
View File
@@ -9,9 +9,6 @@ on:
permissions:
contents: read
env:
HUSKY: 0
jobs:
cargo-check:
# Treat all Rust compiler warnings as errors
@@ -30,7 +27,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
@@ -41,9 +38,9 @@ jobs:
run: rustup target add ${{ matrix.target }}
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "20"
- uses: pnpm/action-setup@v4
name: Install pnpm
+18 -58
View File
@@ -18,11 +18,6 @@ on:
required: false
type: boolean
default: true
run_linux_amd64:
description: "运行 Linux amd64"
required: false
type: boolean
default: true
permissions: write-all
env:
@@ -30,7 +25,6 @@ env:
TAG_CHANNEL: DeployTest
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -56,11 +50,6 @@ jobs:
bundle: nsis
id: windows-arm64
input: run_windows_arm64
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
bundle: deb
id: linux-amd64
input: run_linux_amd64
runs-on: ${{ matrix.os }}
steps:
@@ -70,28 +59,24 @@ jobs:
- name: Checkout Repository
if: github.event.inputs[matrix.input] == 'true'
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@1.91.0
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
if: github.event.inputs[matrix.input] == 'true'
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
if: github.event.inputs[matrix.input] == 'true'
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
workspaces: src-tauri
save-if: false
cache-all-crates: true
cache-workspace-crates: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
shared-key: autobuild-shared
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -101,20 +86,11 @@ jobs:
- name: Install Node
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "20"
cache: "pnpm"
- name: Pnpm Cache
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
lookup-only: true
- name: Pnpm install and check
if: github.event.inputs[matrix.input] == 'true'
run: |
@@ -125,14 +101,6 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
run: pnpm release-version ${{ env.TAG_NAME }}
- name: Add Rust Target
if: github.event.inputs[matrix.input] == 'true'
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build
if: github.event.inputs[matrix.input] == 'true'
uses: tauri-apps/tauri-action@v0
@@ -151,26 +119,18 @@ jobs:
tauriScript: pnpm
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts (macOS)
- name: Upload Artifacts
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
if-no-files-found: error
- name: Upload Artifacts (Windows)
- name: Upload Artifacts
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/nsis/*.exe
if-no-files-found: error
- name: Upload Artifacts (Linux)
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
if-no-files-found: error
@@ -7,38 +7,32 @@ name: Check Formatting
on:
pull_request:
env:
HUSKY: 0
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check Rust changes
id: check_rust
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src-tauri/**'
- '**/*.rs'
- name: Skip if no Rust changes
if: steps.check_rust.outputs.rust != 'true'
run: echo "No Rust changes, skipping rustfmt."
- uses: actions/checkout@v4
- name: install Rust stable and rustfmt
if: steps.check_rust.outputs.rust == 'true'
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: run cargo fmt
if: steps.check_rust.outputs.rust == 'true'
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm format:check
# taplo:
# name: taplo (.toml files)
# runs-on: ubuntu-latest
-75
View File
@@ -1,75 +0,0 @@
name: Frontend Check
on:
pull_request:
workflow_dispatch:
env:
HUSKY: 0
jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check frontend changes
id: check_frontend
uses: dorny/paths-filter@v3
with:
filters: |
frontend:
- 'src/**'
- '**/*.js'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '**/*.json'
- '**/*.md'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'eslint.config.ts'
- 'tsconfig.json'
- 'vite.config.*'
- name: Skip if no frontend changes
if: steps.check_frontend.outputs.frontend != 'true'
run: echo "No frontend changes, skipping frontend checks."
- name: Install pnpm
if: steps.check_frontend.outputs.frontend == 'true'
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v6
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: "24.11.1"
cache: "pnpm"
- name: Restore pnpm cache
if: steps.check_frontend.outputs.frontend == 'true'
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}"
restore-keys: |
pnpm-shared-stable-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
if: steps.check_frontend.outputs.frontend == 'true'
- name: Run Prettier check
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm format:check
- name: Run ESLint
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm lint
- name: Run TypeScript typecheck
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm typecheck
+30 -44
View File
@@ -3,8 +3,6 @@ name: Clippy Lint
on:
pull_request:
workflow_dispatch:
env:
HUSKY: 0
jobs:
clippy:
@@ -21,36 +19,11 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Check src-tauri changes
if: github.event_name != 'workflow_dispatch'
id: check_changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src-tauri/**'
- name: Skip if src-tauri not changed
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust != 'true'
run: echo "No src-tauri changes, skipping clippy lint."
- name: Continue if src-tauri changed
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust == 'true'
run: echo "src-tauri changed, running clippy lint."
- name: Manual trigger - always run
if: github.event_name == 'workflow_dispatch'
run: |
echo "Manual trigger detected: skipping changes check and running clippy."
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: clippy
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -58,13 +31,10 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
workspaces: src-tauri
save-if: false
cache-all-crates: false
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -72,13 +42,29 @@ jobs:
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Run Clippy
working-directory: ./src-tauri
run: cargo clippy-all
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Run Logging Check
working-directory: ./src-tauri
shell: bash
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check
run: |
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
clash-verge-logging-check
pnpm i
pnpm run prebuild ${{ matrix.target }}
# This workflow runs linting using cargo clippy.
# Note: If the web build step is skipped,
# cargo clippy will fail to run due to missing web dist in the Tauri environment.
- name: Build Web Assets
run: pnpm run web:build
env:
NODE_OPTIONS: "--max_old_space_size=4096"
- name: Run Clippy
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
+69 -102
View File
@@ -5,14 +5,12 @@ on:
# ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch:
push:
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
tags:
- "v*.*.*"
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
@@ -24,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -69,11 +67,24 @@ jobs:
needs: [release, release-for-linux-arm, release-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: bash ./scripts/extract_update_logs.sh
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Set Env
@@ -121,7 +132,6 @@ jobs:
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
@@ -132,7 +142,7 @@ jobs:
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
body_path: release.txt
draft: false
prerelease: ${{ contains(github.ref_name, '-rc') }}
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
# generate_release_notes: true
@@ -157,13 +167,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -171,13 +178,8 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
workspaces: src-tauri
save-if: false
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -195,9 +197,9 @@ jobs:
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -209,16 +211,8 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v0.6.0
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -235,7 +229,7 @@ jobs:
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
releaseBody: "Draft release, will be updated later."
releaseDraft: true
prerelease: ${{ contains(github.ref_name, '-rc') }}
prerelease: false
tauriScript: pnpm
args: --target ${{ matrix.target }}
includeUpdaterJson: true
@@ -256,13 +250,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -270,18 +261,13 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
workspaces: src-tauri
save-if: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -337,13 +323,6 @@ jobs:
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -374,10 +353,9 @@ jobs:
name: "Clash Verge Rev v${{env.VERSION}}"
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
files: |
target/${{ matrix.target }}/release/bundle/deb/*.deb
target/${{ matrix.target }}/release/bundle/rpm/*.rpm
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
release-for-fixed-webview2:
name: Release Build for Fixed WebView2
@@ -395,13 +373,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.target }}
uses: actions/checkout@v4
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
@@ -409,18 +381,13 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: "v1-rust"
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
workspaces: src-tauri
save-if: false
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -434,21 +401,14 @@ jobs:
- name: Download WebView2 Runtime
run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Add Rust Target
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0.6.0
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -460,19 +420,19 @@ jobs:
- name: Rename
run: |
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
Rename-Item $file.FullName $newName
@@ -485,8 +445,7 @@ jobs:
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }}
@@ -494,18 +453,17 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update:
if: ${{ !contains(github.ref_name, '-rc') }}
name: Release Update
runs-on: ubuntu-latest
needs: [update_tag]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -521,17 +479,16 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update-for-fixed-webview2:
if: ${{ !contains(github.ref_name, '-rc') }}
runs-on: ubuntu-latest
needs: [update_tag]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -547,13 +504,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
if: ${{ !contains(github.ref_name, '-rc') }}
name: Submit to Winget
runs-on: ubuntu-latest
needs: [update_tag, release-update]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Version
@@ -571,7 +527,6 @@ jobs:
token: ${{ secrets.WINGET_TOKEN }}
notify-telegram:
if: ${{ !contains(github.ref_name, '-rc') }}
name: Notify Telegram
runs-on: ubuntu-latest
needs:
@@ -583,17 +538,30 @@ jobs:
]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: bash ./scripts/extract_update_logs.sh
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -647,7 +615,6 @@ jobs:
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
+6 -9
View File
@@ -2,20 +2,17 @@ name: Updater CI
on: workflow_dispatch
permissions: write-all
env:
HUSKY: 0
jobs:
release-update:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -34,12 +31,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "24.11.1"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
-1
View File
@@ -11,4 +11,3 @@ scripts/_env.sh
.idea
.old
.eslintcache
target
+15 -46
View File
@@ -1,57 +1,26 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
#pnpm pretty-quick --staged
if ! command -v pnpm >/dev/null 2>&1; then
echo "❌ pnpm is required for pre-commit checks."
exit 1
if git diff --cached --name-only | grep -q '^src/'; then
pnpm format:check
if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
exit 1
fi
fi
LOCALE_DIFF="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/locales/' || true)"
if [ -n "$LOCALE_DIFF" ]; then
echo "[pre-commit] Locale changes detected. Regenerating i18n types..."
pnpm i18n:types
if [ -d src/types/generated ]; then
echo "[pre-commit] Staging regenerated i18n type artifacts..."
git add src/types/generated
fi
fi
echo "[pre-commit] Running pnpm format before lint..."
pnpm format
echo "[pre-commit] Running lint-staged for JS/TS files..."
pnpm exec lint-staged
RUST_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src-tauri/.*\.rs$' || true)"
if [ -n "$RUST_FILES" ]; then
echo "[pre-commit] Formatting Rust changes with cargo fmt..."
(
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cd src-tauri
cargo fmt
)
while IFS= read -r file; do
[ -n "$file" ] && git add "$file"
done <<< "$RUST_FILES"
echo "[pre-commit] Linting Rust changes with cargo clippy..."
(
cd src-tauri
cargo clippy-all
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
echo "[pre-commit] Installing clash-verge-logging-check..."
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
clash-verge-logging-check
)
cd ..
fi
TS_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx)$' || true)"
if [ -n "$TS_FILES" ]; then
echo "[pre-commit] Running TypeScript type check..."
pnpm typecheck
fi
#git add .
echo "[pre-commit] All checks completed successfully."
# 允许提交
exit 0
+25 -33
View File
@@ -1,42 +1,34 @@
#!/bin/bash
set -euo pipefail
remote_name="${1:-origin}"
remote_url="${2:-unknown}"
# $1: remote name (e.g., origin)
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
if ! command -v pnpm >/dev/null 2>&1; then
echo "❌ pnpm is required for pre-push checks."
exit 1
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cargo clippy --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then
echo "Clippy found issues in src-tauri. Please fix them before pushing."
exit 1
fi
fi
echo "[pre-push] Preparing to push to '$remote_name' ($remote_url). Running full validation..."
echo "[pre-push] Checking Prettier formatting..."
pnpm format:check
echo "[pre-push] Running ESLint..."
pnpm lint
echo "[pre-push] Running TypeScript type checking..."
pnpm typecheck
if command -v cargo >/dev/null 2>&1; then
echo "[pre-push] Verifying Rust formatting..."
(
cd src-tauri
cargo fmt --check
)
echo "[pre-push] Running cargo clippy..."
(
cd src-tauri
cargo clippy-all
)
# Only run format check if the remote exists and is the main repo
remote_name="$1"
if git remote get-url "$remote_name" >/dev/null 2>&1; then
remote_url=$(git remote get-url "$remote_name")
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
echo "[pre-push] Running pnpm format:check..."
pnpm format:check
if [ $? -ne 0 ]; then
echo "❌ Code format check failed. Please fix formatting before pushing."
exit 1
fi
else
echo "[pre-push] Not pushing to target repo. Skipping format check."
fi
else
echo "[pre-push] ⚠️ cargo not found; skipping Rust checks."
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
fi
echo "[pre-push] All checks passed."
exit 0
+1 -3
View File
@@ -1,10 +1,8 @@
# README.md
# Changelog.md
# UPDATELOG.md
# CONTRIBUTING.md
pnpm-lock.yaml
src-tauri/target/
src-tauri/gen/
target
+1 -1
View File
@@ -11,6 +11,6 @@
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "auto",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto"
}
+77 -62
View File
@@ -1,132 +1,147 @@
# CONTRIBUTING
Thank you for your interest in contributing to **Clash Verge Rev**! This guide provides instructions to help you set up your development environment and start contributing effectively.
## Internationalization (i18n)
We welcome translations and improvements to existing locales. For details on contributing translations, please see [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md).
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
## Development Setup
Before contributing, you need to set up your development environment. Follow the steps below carefully.
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
### Prerequisites
1. **Install Rust and Node.js**
Our project requires both Rust and Node.js. Follow the official installation instructions [here](https://tauri.app/start/prerequisites/).
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system.
### Windows Users
### Setup for Windows Users
> [!NOTE]
> **Windows ARM users must also install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the corresponding environment variables.**
> The `ring` crate depends on `clang` when building on Windows ARM.
> [!NOTE]
> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.**
>
> Because the `ring` crate is compiled based on `clang` under Windows ARM.
Additional steps for Windows:
If you're a Windows user, you may need to perform some additional steps:
- Ensure Rust and Node.js are added to your system `PATH`.
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
- The gnu `patch` tool should be installed
- Install the GNU `patch` tool.
When you setup `Rust` environment, Only use toolchain with `Windows MSVC` , to change settings follow command:
- Use the MSVC toolchain for Rust:
```bash
```shell
rustup target add x86_64-pc-windows-msvc
rustup set default-host x86_64-pc-windows-msvc
```
### Install Node.js Package Manager
### Install Node.js Package
Enable `corepack`:
After installing Rust and Node.js, install the necessary Node.js and Node Package Manager:
```bash
corepack enable
```shell
npm install pnpm -g
```
### Install Project Dependencies
### Install Dependencies
Node.js dependencies:
Install node packages
```bash
```shell
pnpm install
```
Ubuntu-only system packages:
Install apt packages ONLY for Ubuntu
```bash
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
```shell
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
```
### Download the Mihomo Core Binary (Automatic)
### Download the Mihomo Core Binary
```bash
pnpm run prebuild
pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries
```
You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run prebuild
# Use '--force' or '-f' to update both the Mihomo core version
# and the Clash Verge Rev service version to the latest available.
pnpm run prebuild --force
```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
### Run the Development Server
```bash
pnpm dev # Standard
pnpm dev:diff # If an app instance already exists
pnpm dev:tauri # Run Tauri development mode
To run the development server, use the following command:
```shell
pnpm dev
# If an app instance already exists, use a different command
pnpm dev:diff
# To using tauri built-in dev tool
pnpm dev:tauri
```
### Build the Project
Standard build:
To build this project:
```bash
```shell
pnpm build
```
Fast build for testing:
For a faster build, use the following command
```bash
```shell
pnpm build:fast
```
### Clean Build
This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly.
```bash
The `Artifacts` will display in the `log` in the Terminal.
### Build clean
To clean rust build:
```shell
pnpm clean
```
### Portable Version (Windows Only)
```bash
To package portable version after the build:
```shell
pnpm portable
```
## Contributing Your Changes
### Before Committing
#### Before commit your changes
**Code quality checks:**
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
1. Code quailty checks
```bash
# Rust backend
cargo clippy-all
# Frontend
pnpm lint
# For rust backend
$ clash-verge-rev: pnpm clippy
# For frontend (not yet).
```
**Code formatting:**
2. Code style formatting
```bash
# Rust backend
cargo fmt
# Frontend
pnpm format
# For rust backend
$ clash-verge-rev: cd src-tauri
$ clash-verge-rev/src-tauri: cargo fmt
# For frontend
$ clash-verge-rev: pnpm format:check
$ clash-verge-rev: pnpm format
```
### Submitting Your Changes
Once you have made your changes:
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Commit your changes with clear and concise commit messages.
4. Push your branch to your fork and submit a pull request to our repository.
3. Commit your changes with clear messages.
4. Push your branch and submit a pull request.
We appreciate your contributions and look forward to your participation!
We appreciate your contributions and look forward to your active participation in our project!
-134
View File
@@ -1,134 +0,0 @@
[workspace]
members = [
"src-tauri",
"crates/clash-verge-draft",
"crates/clash-verge-logging",
"crates/clash-verge-signal",
"crates/tauri-plugin-clash-verge-sysinfo",
"crates/clash-verge-types",
]
resolver = "2"
[workspace.package]
edition = "2024"
rust-version = "1.91"
[profile.release]
panic = "abort"
codegen-units = 1
lto = "thin"
opt-level = 3
debug = false
strip = true
overflow-checks = false
rpath = false
[profile.dev]
incremental = true
codegen-units = 64
opt-level = 0
debug = true
strip = "none"
overflow-checks = true
lto = false
rpath = false
[profile.fast-release]
inherits = "release"
codegen-units = 64
incremental = true
lto = false
opt-level = 0
debug = true
strip = false
[workspace.dependencies]
clash-verge-draft = { path = "crates/clash-verge-draft" }
clash-verge-logging = { path = "crates/clash-verge-logging" }
clash-verge-signal = { path = "crates/clash-verge-signal" }
clash-verge-types = { path = "crates/clash-verge-types" }
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tauri = { version = "2.9.4" }
tauri-plugin-clipboard-manager = "2.3.2"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.100"
criterion = { version = "0.7.0", features = ["async_tokio"] }
tokio = { version = "1.48.0", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
flexi_logger = "0.31.7"
log = "0.4.29"
smartstring = { version = "1.0.1" }
compact_str = { version = "0.9.0", features = ["serde"] }
serde = { version = "1.0.228" }
serde_json = { version = "1.0.145" }
serde_yaml_ng = { version = "0.10.0" }
# *** For Windows platform only ***
deelevate = "0.2.0"
# *********************************
[workspace.lints.clippy]
correctness = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
panic = "deny"
unimplemented = "deny"
todo = "warn"
dbg_macro = "warn"
clone_on_ref_ptr = "warn"
rc_clone_in_vec_init = "warn"
large_stack_arrays = "warn"
large_const_arrays = "warn"
async_yields_async = "deny"
mutex_atomic = "deny"
mutex_integer = "deny"
rc_mutex = "deny"
unused_async = "deny"
await_holding_lock = "deny"
large_futures = "deny"
future_not_send = "deny"
redundant_else = "deny"
needless_continue = "deny"
needless_raw_string_hashes = "deny"
or_fun_call = "deny"
cognitive_complexity = "deny"
useless_let_if_seq = "deny"
use_self = "deny"
tuple_array_conversions = "deny"
trait_duplication_in_bounds = "deny"
suspicious_operation_groupings = "deny"
string_lit_as_bytes = "deny"
significant_drop_tightening = "deny"
significant_drop_in_scrutinee = "deny"
redundant_clone = "deny"
# option_if_let_else = "deny" // 过于激进,暂时不开启
needless_pass_by_ref_mut = "deny"
needless_collect = "deny"
missing_const_for_fn = "deny"
iter_with_drain = "deny"
iter_on_single_items = "deny"
iter_on_empty_collections = "deny"
# fallible_impl_from = "deny" // 过于激进,暂时不开启
equatable_if_let = "deny"
collection_is_never_read = "deny"
branches_sharing_code = "deny"
pathbuf_init_then_push = "deny"
option_as_ref_cloned = "deny"
large_types_passed_by_value = "deny"
# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启
expl_impl_clone_on_copy = "deny"
copy_iterator = "deny"
cloned_instead_of_copied = "deny"
# self_only_used_in_recursion = "deny" // Since 1.92.0
unnecessary_self_imports = "deny"
unused_trait_names = "deny"
wildcard_imports = "deny"
unnecessary_wraps = "deny"
-287
View File
@@ -1,287 +0,0 @@
## v2.4.4
- **Mihomo(Meta) 内核升级至 v1.19.17**
### 🐞 修复问题
- Linux 无法切换 TUN 堆栈
- macOS service 启动项显示名称(试验性修改)
- macOS 非预期 Tproxy 端口设置
- 流量图缩放异常
- PAC 自动代理脚本内容无法动态调整
- 兼容从旧版服务模式升级
- Monaco 编辑器的行数上限
- 已删除节点在手动分组中导致配置无法加载
- 仪表盘与托盘状态不同步
- 彻底修复 macOS 连接页面显示异常
- windows 端监听关机信号失败
- 修复代理按钮和高亮状态不同步
- 修复侧边栏可能的未能正确跳转
- 修复解锁测试部分地区图标编码不正确
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
- 修复在搜索框输入不完整正则直接崩溃
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
- 修复更新时加载进度条异常
- 升级内核失败导致内核不可用问题
- 修复 macOS 在安装和卸载服务时提示与操作不匹配
- 修复菜单排序模式拖拽异常
- 修复托盘菜单代理组前的异常勾选状态
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 支持连接页面各个项目的排序
- 实现可选的自动备份
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
- 日志页面支持按时间倒序
- 增加「重新激活订阅」的全局快捷键
- WebView2 Runtime 修复构建升级到 133.0.3065.92
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 网络请求改为使用 rustls,提升 TLS 兼容性
- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入
- 替换前端信息编辑组件,提供更好性能
- 优化后端内存和性能表现
- 防止退出时可能的禁用 TUN 失败
- 全新 i18n 支持方式
- 优化备份设置布局
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
- 性能优化系统状态获取
- 优化托盘菜单当前订阅检测逻辑
- 优化连接页面表格渲染
- 优化链式代理 UI 反馈
- 优化重启应用的资源清理逻辑
- 优化前端数据刷新
- 优化流量采样和数据处理
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
- 优化前端 WebSocket 连接机制
- 改进旧版 Service 需要重新安装检测流程
- 优化 macOS, Linux 和 Windows 系统信号处理
</details>
## v2.4.3
**发行代号:澜**
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复前端无法及时刷新操作状态
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
- 修复一键导入订阅导致应用卡死崩溃的问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- **Mihomo(Meta) 内核升级至 v1.19.15**
- 支持前端修改日志(最大文件大小、最大保留数量)
- 新增链式代理图形化设置功能
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
- 监听关机事件,自动关闭系统代理
- 主界面“当前节点”卡片新增“延迟测试”按钮
- 新增批量选择配置文件功能
- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置
- 新增本地备份功能
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
- 允许独立控制订阅自动更新
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
- 改进 macos 下系统代理设置的方法
- 优化 TUN 模式可用性的判断
- 移除流媒体检测的系统级提示(使用软件内通知)
- 优化后端 i18n 资源占用
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
- 修改内核默认日志级别为 Info
- 支持通过桌面快捷方式重新打开应用
- 支持订阅界面输入链接后回车导入
- 选择按延迟排序时每次延迟测试自动刷新节点顺序
- 配置重载失败时自动重启核心
- 启用 TUN 前等待服务就绪
- 卸载 TUN 时会先关闭
- 优化应用启动页
- 优化首页当前节点对MATCH规则的支持
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
- 添加热键绑定错误的提示信息
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
</details>
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
- 优化热键响应速度,提升快捷键操作体验
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题
- 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
- 修复卸载服务后的 tun 开关状态问题
- 修复页面快速切换订阅时导致崩溃
- 修复丢失工作目录时无法恢复环境
- 修复从轻量模式恢复导致崩溃
### 👙 界面样式
- 统一代理设置样式
### 🗑️ 移除内容
- 移除启动阶段自动清理过期订阅
## v2.4.0
**发行代号:融**
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
### 🏆 重大改进
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.12**
- 新增版本信息复制按钮
- 增强型流量监控,支持更详细的数据分析
- 新增流量图表多种显示模式
- 新增强制刷新配置和节点缓存功能
- 首页流量统计支持查看刻度线详情
### 🚀 性能优化
- 全面提升数据传输和处理效率
- 优化内存使用,减少系统资源消耗
- 改进流量图表渲染性能
- 优化配置和节点刷新策略,从5秒延长到60秒
- 改进数据缓存机制,减少重复请求
- 优化异步程序性能
### 🐞 修复问题
- 修复系统代理状态检测和显示不一致问题
- 修复系统主题窗口颜色不一致问题
- 修复特殊字符 URL 处理问题
- 修复配置修改后缓存不同步问题
- 修复 Windows 安装器自启设置问题
- 修复 macOS 下 Dock 图标恢复窗口问题
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
- 修复架构升级后节点测速功能异常
- 修复架构升级后流量统计功能异常
- 修复架构升级后日志功能异常
- 修复外部控制器跨域配置保存问题
- 修复首页端口显示不一致问题
- 修复首页流量统计刻度线显示问题
- 修复日志页面按钮功能混淆问题
- 修复日志等级设置保存问题
- 修复日志等级异常过滤
- 修复清理日志天数功能异常
- 修复偶发性启动卡死问题
- 修复首页虚拟网卡开关在管理模式下的状态问题
### 🔧 技术改进
- 统一使用新的内核通信方式
- 新增外部控制器配置界面
- 改进跨平台兼容性支持
-10
View File
@@ -9,16 +9,6 @@
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
</h3>
<p align="center">
Languages:
<a href="./README.md">简体中文</a> ·
<a href="./docs/README_en.md">English</a> ·
<a href="./docs/README_es.md">Español</a> ·
<a href="./docs/README_ru.md">Русский</a> ·
<a href="./docs/README_ja.md">日本語</a> ·
<a href="./docs/README_ko.md">한국어</a>
</p>
## Preview
| Dark | Light |
+144
View File
@@ -1,3 +1,147 @@
## v2.4.3
### 🚀 优化改进
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
- 优化热键响应速度,提升快捷键操作体验
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题
- 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
- 修复卸载服务后的 tun 开关状态问题
- 修复页面快速切换订阅时导致崩溃
- 修复丢失工作目录时无法恢复环境
- 修复从轻量模式恢复导致崩溃
### 👙 界面样式
- 统一代理设置样式
### 🗑️ 移除内容
- 移除启动阶段自动清理过期订阅
## v2.4.0
**发行代号:融**
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
### 🏆 重大改进
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.12**
- 新增版本信息复制按钮
- 增强型流量监控,支持更详细的数据分析
- 新增流量图表多种显示模式
- 新增强制刷新配置和节点缓存功能
- 首页流量统计支持查看刻度线详情
### 🚀 性能优化
- 全面提升数据传输和处理效率
- 优化内存使用,减少系统资源消耗
- 改进流量图表渲染性能
- 优化配置和节点刷新策略,从5秒延长到60秒
- 改进数据缓存机制,减少重复请求
- 优化异步程序性能
### 🐞 修复问题
- 修复系统代理状态检测和显示不一致问题
- 修复系统主题窗口颜色不一致问题
- 修复特殊字符 URL 处理问题
- 修复配置修改后缓存不同步问题
- 修复 Windows 安装器自启设置问题
- 修复 macOS 下 Dock 图标恢复窗口问题
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
- 修复架构升级后节点测速功能异常
- 修复架构升级后流量统计功能异常
- 修复架构升级后日志功能异常
- 修复外部控制器跨域配置保存问题
- 修复首页端口显示不一致问题
- 修复首页流量统计刻度线显示问题
- 修复日志页面按钮功能混淆问题
- 修复日志等级设置保存问题
- 修复日志等级异常过滤
- 修复清理日志天数功能异常
- 修复偶发性启动卡死问题
- 修复首页虚拟网卡开关在管理模式下的状态问题
### 🔧 技术改进
- 统一使用新的内核通信方式
- 新增外部控制器配置界面
- 改进跨平台兼容性支持
## v2.3.2
### 🐞 修复问题
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "clash-verge-draft"
version = "0.1.0"
edition = "2024"
[[bench]]
name = "draft_bench"
path = "bench/benche_me.rs"
harness = false
[dependencies]
parking_lot = { workspace = true }
anyhow = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
tokio = { workspace = true }
-119
View File
@@ -1,119 +0,0 @@
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;
use std::process;
use tokio::runtime::Runtime;
use clash_verge_draft::Draft;
#[derive(Default, Clone, Debug)]
struct IVerge {
enable_auto_launch: Option<bool>,
enable_tun_mode: Option<bool>,
}
fn make_draft() -> Draft<IVerge> {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
};
Draft::new(verge)
}
pub fn bench_draft(c: &mut Criterion) {
let rt = Runtime::new().unwrap_or_else(|e| {
eprintln!("Tokio runtime init failed: {e}");
process::exit(1);
});
let mut group = c.benchmark_group("draft");
group.sample_size(100);
group.warm_up_time(std::time::Duration::from_millis(300));
group.measurement_time(std::time::Duration::from_secs(1));
group.bench_function("data_mut", |b| {
b.iter(|| {
let draft = black_box(make_draft());
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
black_box(&draft.latest_arc().enable_tun_mode);
});
});
group.bench_function("draft_mut_first", |b| {
b.iter(|| {
let draft = black_box(make_draft());
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
});
});
group.bench_function("draft_mut_existing", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
});
let latest1 = draft.latest_arc();
black_box(&latest1.enable_tun_mode);
}
draft.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
let latest2 = draft.latest_arc();
black_box(&latest2.enable_tun_mode);
});
});
group.bench_function("latest_arc", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
});
});
group.bench_function("apply", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
}
draft.apply();
black_box(&draft);
});
});
group.bench_function("discard", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
}
draft.discard();
black_box(&draft);
});
});
group.bench_function("with_data_modify_async", |b| {
b.to_async(&rt).iter(|| async {
let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _>(|mut box_data| async move {
box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ()))
})
.await;
});
});
group.finish();
}
criterion_group!(benches, bench_draft);
criterion_main!(benches);
-102
View File
@@ -1,102 +0,0 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
}
impl<T: Clone> Draft<T> {
#[inline]
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
#[inline]
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc,不 clone T
#[inline]
pub fn latest_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
}
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
#[inline]
pub fn edit_draft<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut T) -> R,
{
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
let mut guard = self.inner.write();
let mut draft_arc = if guard.1.is_none() {
Arc::clone(&guard.0)
} else {
#[allow(clippy::unwrap_used)]
guard.1.take().unwrap()
};
drop(guard);
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &mut T
let result = f(&mut **boxed);
// 恢复修改后的草稿 Arc
self.inner.write().1 = Some(draft_arc);
result
}
/// 将草稿提交到已提交位置(替换),并清除草稿
#[inline]
pub fn apply(&self) {
let mut guard = self.inner.write();
if let Some(d) = guard.1.take() {
guard.0 = d;
}
}
/// 丢弃草稿(如果存在)
#[inline]
pub fn discard(&self) {
let mut guard = self.inner.write();
guard.1 = None;
}
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
#[inline]
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
{
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
let local: Box<T> = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
(*guard.0).clone()
};
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc)
self.inner.write().0 = Arc::new(new_local);
Ok(res)
}
}
-269
View File
@@ -1,269 +0,0 @@
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use clash_verge_draft::Draft;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
#[derive(Clone, Debug, Default, PartialEq)]
struct IVerge {
enable_auto_launch: Option<bool>,
enable_tun_mode: Option<bool>,
}
// Minimal single-threaded executor for immediately-ready futures
fn block_on_ready<F: Future>(fut: F) -> F::Output {
fn no_op_raw_waker() -> RawWaker {
fn clone(_: *const ()) -> RawWaker {
no_op_raw_waker()
}
fn wake(_: *const ()) {}
fn wake_by_ref(_: *const ()) {}
fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
RawWaker::new(std::ptr::null(), &VTABLE)
}
let waker = unsafe { Waker::from_raw(no_op_raw_waker()) };
let mut cx = Context::from_waker(&waker);
let mut fut = Box::pin(fut);
loop {
match Pin::as_mut(&mut fut).poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[test]
fn test_draft_basic_flow() {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
};
let draft = Draft::new(verge);
// 读取正式数据(data_arc)
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 修改草稿(使用 edit_draft)
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
});
// 正式数据未变
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 草稿已变
{
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 提交草稿
draft.apply();
// 正式数据已更新
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
// 新一轮草稿并修改
draft.edit_draft(|d| {
d.enable_auto_launch = Some(true);
});
{
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(true));
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 丢弃草稿
draft.discard();
// 丢弃后再次创建草稿,会从已提交重新 clone
{
draft.edit_draft(|d| {
// 原 committed 是 enable_auto_launch = Some(false)
assert_eq!(d.enable_auto_launch, Some(false));
// 再修改一下
d.enable_tun_mode = Some(false);
});
// 草稿中值已修改,但正式数据仍是 apply 后的值
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
}
#[test]
fn test_arc_pointer_behavior_on_edit_and_apply() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
});
// 初始 latest == committed
let committed = draft.data_arc();
let latest = draft.latest_arc();
assert!(std::sync::Arc::ptr_eq(&committed, &latest));
// 第一次 edit:由于与 committed 共享,Arc::make_mut 会克隆
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
let committed_after_first_edit = draft.data_arc();
let draft_after_first_edit = draft.latest_arc();
assert!(!std::sync::Arc::ptr_eq(
&committed_after_first_edit,
&draft_after_first_edit
));
// 提交会把 committed 指向草稿的 Arc
let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit);
draft.apply();
let committed_after_apply = draft.data_arc();
assert_eq!(
std::sync::Arc::as_ptr(&committed_after_apply),
prev_draft_ptr
);
// 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆
// 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest1 = draft.latest_arc();
let latest1_ptr = std::sync::Arc::as_ptr(&latest1);
drop(latest1); // 确保只有 Draft 内部持有草稿 Arc
// 再次编辑(unique,Arc::make_mut 不应克隆)
draft.edit_draft(|d| d.enable_tun_mode = Some(false));
let latest2 = draft.latest_arc();
let latest2_ptr = std::sync::Arc::as_ptr(&latest2);
assert_eq!(latest1_ptr, latest2_ptr, "Unique edit should not clone Arc");
assert_eq!(latest2.enable_auto_launch, Some(false));
assert_eq!(latest2.enable_tun_mode, Some(false));
}
#[test]
fn test_discard_restores_latest_to_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 创建草稿并修改
draft.edit_draft(|d| d.enable_auto_launch = Some(true));
let committed = draft.data_arc();
let latest = draft.latest_arc();
assert!(!std::sync::Arc::ptr_eq(&committed, &latest));
// 丢弃草稿后 latest 应回到 committed
draft.discard();
let committed2 = draft.data_arc();
let latest2 = draft.latest_arc();
assert!(std::sync::Arc::ptr_eq(&committed2, &latest2));
assert_eq!(latest2.enable_auto_launch, Some(false));
}
#[test]
fn test_edit_draft_returns_closure_result() {
let draft = Draft::new(IVerge::default());
let ret = draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
123usize
});
assert_eq!(ret, 123);
let latest = draft.latest_arc();
assert_eq!(latest.enable_tun_mode, Some(true));
}
#[test]
fn test_with_data_modify_ok_and_replaces_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 使用 with_data_modify 异步(立即就绪)地更新 committed
let res = block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(true);
Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
}));
assert_eq!(
{
#[allow(clippy::unwrap_used)]
res.unwrap()
},
"done"
);
let committed = draft.data_arc();
assert_eq!(committed.enable_auto_launch, Some(true));
assert_eq!(committed.enable_tun_mode, Some(false));
}
#[test]
fn test_with_data_modify_error_propagation() {
let draft = Draft::new(IVerge::default());
#[allow(clippy::unwrap_used)]
let err = block_on_ready(draft.with_data_modify(|v| async move {
drop(v);
Err::<(Box<IVerge>, ()), _>(anyhow!("boom"))
}))
.unwrap_err();
assert_eq!(format!("{err}"), "boom");
}
#[test]
fn test_with_data_modify_does_not_touch_existing_draft() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 创建草稿并修改
draft.edit_draft(|d| {
d.enable_auto_launch = Some(true);
d.enable_tun_mode = Some(true);
});
let draft_before = draft.latest_arc();
let draft_before_ptr = std::sync::Arc::as_ptr(&draft_before);
// 同时通过 with_data_modify 修改 committed
#[allow(clippy::unwrap_used)]
block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(false); // 与草稿不同
Ok((Box::new(*v), ())) // Dereference v to get Box<T>
}))
.unwrap();
// 草稿应保持不变
let draft_after = draft.latest_arc();
assert_eq!(
std::sync::Arc::as_ptr(&draft_after),
draft_before_ptr,
"Existing draft should not be replaced by with_data_modify"
);
assert_eq!(draft_after.enable_auto_launch, Some(true));
assert_eq!(draft_after.enable_tun_mode, Some(true));
// 丢弃草稿后 latest == committed,且 committed 为异步修改结果
draft.discard();
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(false));
}
}
-14
View File
@@ -1,14 +0,0 @@
[package]
name = "clash-verge-logging"
version = "0.1.0"
edition = "2024"
[dependencies]
log = { workspace = true }
tokio = { workspace = true }
compact_str = { workspace = true }
flexi_logger = { workspace = true }
[features]
default = []
tauri-dev = []
-139
View File
@@ -1,139 +0,0 @@
use compact_str::CompactString;
use flexi_logger::DeferredNow;
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::filter::LogLineFilter;
use flexi_logger::writers::FileLogWriter;
use flexi_logger::writers::LogWriter as _;
use log::Level;
use log::Record;
use std::{fmt, sync::Arc};
use tokio::sync::{Mutex, MutexGuard};
pub type SharedWriter = Arc<Mutex<FileLogWriter>>;
#[derive(Debug, PartialEq, Eq)]
pub enum Type {
Cmd,
Core,
Config,
Setup,
System,
SystemSignal,
Service,
Hotkey,
Window,
Tray,
Timer,
Frontend,
Backup,
File,
Lightweight,
Network,
ProxyMode,
Validate,
ClashVergeRev,
}
impl fmt::Display for Type {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cmd => write!(f, "[Cmd]"),
Self::Core => write!(f, "[Core]"),
Self::Config => write!(f, "[Config]"),
Self::Setup => write!(f, "[Setup]"),
Self::System => write!(f, "[System]"),
Self::SystemSignal => write!(f, "[SysSignal]"),
Self::Service => write!(f, "[Service]"),
Self::Hotkey => write!(f, "[Hotkey]"),
Self::Window => write!(f, "[Window]"),
Self::Tray => write!(f, "[Tray]"),
Self::Timer => write!(f, "[Timer]"),
Self::Frontend => write!(f, "[Frontend]"),
Self::Backup => write!(f, "[Backup]"),
Self::File => write!(f, "[File]"),
Self::Lightweight => write!(f, "[Lightweight]"),
Self::Network => write!(f, "[Network]"),
Self::ProxyMode => write!(f, "[ProxMode]"),
Self::Validate => write!(f, "[Validate]"),
Self::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
}
}
#[macro_export]
macro_rules! logging {
// 不带 print 参数的版本(默认不打印)
($level:ident, $type:expr, $($arg:tt)*) => {
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! logging_error {
// Handle Result<T, E>
($type:expr, $expr:expr) => {
if let Err(err) = $expr {
log::error!(target: "app", "[{}] {}", $type, err);
}
};
// Handle formatted message: always print to stdout and log as error
($type:expr, $fmt:literal $(, $arg:expr)*) => {
log::error!(target: "app", "[{}] {}", $type, format_args!($fmt $(, $arg)*));
};
}
#[inline]
pub fn write_sidecar_log(
writer: MutexGuard<'_, FileLogWriter>,
now: &mut DeferredNow,
level: Level,
message: &CompactString,
) {
let args = format_args!("{}", message);
let record = Record::builder()
.args(args)
.level(level)
.target("sidecar")
.build();
let _ = writer.write(now, &record);
}
#[cfg(not(feature = "tauri-dev"))]
pub struct NoModuleFilter<'a>(pub &'a [&'a str]);
#[cfg(not(feature = "tauri-dev"))]
impl<'a> NoModuleFilter<'a> {
#[inline]
pub fn filter(&self, record: &Record) -> bool {
if let Some(module) = record.module_path() {
for blocked in self.0 {
if module.len() >= blocked.len()
&& module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..]
{
return false;
}
}
}
true
}
}
#[cfg(not(feature = "tauri-dev"))]
impl<'a> LogLineFilter for NoModuleFilter<'a> {
#[inline]
fn write(
&self,
now: &mut DeferredNow,
record: &Record,
writer: &dyn flexi_logger::filter::LogLineWriter,
) -> std::io::Result<()> {
if !self.filter(record) {
return Ok(());
}
writer.write(now, record)
}
}
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "clash-verge-signal"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
[dependencies]
clash-verge-logging = { workspace = true }
log = { workspace = true }
tokio = { workspace = true }
[lints]
workspace = true
-35
View File
@@ -1,35 +0,0 @@
use std::sync::OnceLock;
use clash_verge_logging::{Type, logging};
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future + Send + 'static,
{
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
Ok(rt) => Some(rt),
Err(e) => {
logging!(
info,
Type::SystemSignal,
"register shutdown signal failed, create tokio runtime error: {}",
e
);
None
}
});
#[cfg(unix)]
unix::register(f);
#[cfg(windows)]
windows::register(f);
}
-94
View File
@@ -1,94 +0,0 @@
use std::sync::atomic::{AtomicBool, Ordering};
use clash_verge_logging::{Type, logging};
use tokio::signal::unix::{SignalKind, signal};
use crate::RUNTIME;
static IS_CLEANING_UP: AtomicBool = AtomicBool::new(false);
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future + Send + 'static,
{
if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move {
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGTERM: {}",
e
);
return;
}
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGINT: {}",
e
);
return;
}
};
let mut sighup = match signal(SignalKind::hangup()) {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register SIGHUP: {}",
e
);
return;
}
};
loop {
let signal_name;
tokio::select! {
_ = sigterm.recv() => {
signal_name = "SIGTERM";
}
_ = sigint.recv() => {
signal_name = "SIGINT";
}
_ = sighup.recv() => {
signal_name = "SIGHUP";
}
else => {
break;
}
}
if IS_CLEANING_UP.load(Ordering::SeqCst) {
logging!(
info,
Type::SystemSignal,
"Already shutting down, ignoring repeated signal: {}",
signal_name
);
continue;
}
IS_CLEANING_UP.store(true, Ordering::SeqCst);
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
f().await;
}
});
} else {
logging!(
error,
Type::SystemSignal,
"register shutdown signal failed, RUNTIME is not available"
);
}
}
-114
View File
@@ -1,114 +0,0 @@
use std::sync::atomic::{AtomicBool, Ordering};
use clash_verge_logging::{Type, logging};
use tokio::signal::windows;
use crate::RUNTIME;
static IS_CLEANING_UP: AtomicBool = AtomicBool::new(false);
pub fn register<F, Fut>(f: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future + Send + 'static,
{
if let Some(Some(rt)) = RUNTIME.get() {
rt.spawn(async move {
let mut ctrl_c = match windows::ctrl_c() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+C: {}",
e
);
return;
}
};
let mut ctrl_close = match windows::ctrl_close() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Close: {}",
e
);
return;
}
};
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Shutdown: {}",
e
);
return;
}
};
let mut ctrl_logoff = match windows::ctrl_logoff() {
Ok(s) => s,
Err(e) => {
logging!(
error,
Type::SystemSignal,
"Failed to register Ctrl+Logoff: {}",
e
);
return;
}
};
loop {
let signal_name;
tokio::select! {
_ = ctrl_c.recv() => {
signal_name = "Ctrl+C";
}
_ = ctrl_close.recv() => {
signal_name = "Ctrl+Close";
}
_ = ctrl_shutdown.recv() => {
signal_name = "Ctrl+Shutdown";
}
_ = ctrl_logoff.recv() => {
signal_name = "Ctrl+Logoff";
}
}
if IS_CLEANING_UP.load(Ordering::SeqCst) {
logging!(
info,
Type::SystemSignal,
"Already shutting down, ignoring repeated signal: {}",
signal_name
);
continue;
}
IS_CLEANING_UP.store(true, Ordering::SeqCst);
logging!(
info,
Type::SystemSignal,
"Caught Windows signal: {}",
signal_name
);
f().await;
}
});
} else {
logging!(
error,
Type::SystemSignal,
"register shutdown signal failed, RUNTIME is not available"
);
}
}
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "clash-verge-types"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
[dependencies]
serde = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true }
[lints]
workspace = true
-1
View File
@@ -1 +0,0 @@
pub mod runtime;
-152
View File
@@ -1,152 +0,0 @@
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
#[derive(Default, Clone)]
pub struct IRuntime {
pub config: Option<Mapping>,
// 记录在订阅中(包括merge和script生成的)出现过的keys
// 这些keys不一定都生效
pub exists_keys: HashSet<String>,
// TODO 或许可以用 FixMap 来存储以提升效率
pub chain_logs: HashMap<String, Vec<(String, String)>>,
}
impl IRuntime {
#[inline]
pub fn new() -> Self {
Self::default()
}
// 这里只更改 allow-lan | ipv6 | log-level | tun
#[inline]
pub fn patch_config(&mut self, patch: &Mapping) {
let config = if let Some(config) = self.config.as_mut() {
config
} else {
return;
};
for key in PATCH_CONFIG_INNER.iter() {
if let Some(value) = patch.get(key) {
config.insert((*key).into(), value.clone());
}
}
let patch_tun = patch.get("tun");
if let Some(patch_tun_value) = patch_tun {
let mut tun = config
.get("tun")
.and_then(|val| val.as_mapping())
.cloned()
.unwrap_or_else(Mapping::new);
if let Some(patch_tun_mapping) = patch_tun_value.as_mapping() {
for key in use_keys(patch_tun_mapping) {
if let Some(value) = patch_tun_mapping.get(key.as_str()) {
tun.insert(Value::from(key.as_str()), value.clone());
}
}
}
config.insert("tun".into(), Value::from(tun));
}
}
/// 更新链式代理配置
///
/// 该函数更新 `proxies` 和 `proxy-groups` 配置,并处理链式代理的修改或(传入 None )删除。
///
/// 配置示例:
///
/// ```json
/// {
/// "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": ["出口节点"]
/// }
/// ]
/// }
/// ```
#[inline]
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
let config = if let Some(config) = self.config.as_mut() {
config
} else {
return;
};
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");
}
});
}
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config
&& 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))
&& i != 0
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
{
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
}
}
}
}
}
// TODO 完整迁移 enhance 行为后移除
#[inline]
fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
config
.iter()
.filter_map(|(key, _)| key.as_str())
.map(|s: &str| {
let mut s: String = s.into();
s.make_ascii_lowercase();
s
})
}
@@ -1,20 +0,0 @@
[package]
name = "tauri-plugin-clash-verge-sysinfo"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
[dependencies]
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
sysinfo = { version = "0.37.2", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.178"
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
[lints]
workspace = true
@@ -1,39 +0,0 @@
use parking_lot::RwLock;
use tauri::{AppHandle, Runtime, State, command};
use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
use crate::Platform;
// TODO 迁移,让新的结构体允许通过 tauri command 正确使用 structure.field 方式获取信息
#[command]
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<String, Error> {
Ok(state.inner().read().to_string())
}
/// 获取应用的运行时间(毫秒)
#[command]
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
Ok(state
.inner()
.read()
.appinfo
.app_startup_time
.elapsed()
.as_millis())
}
/// 检查应用是否以管理员身份运行
#[command]
pub fn app_is_admin(state: State<'_, RwLock<Platform>>) -> Result<bool, Error> {
Ok(state.inner().read().appinfo.app_is_admin)
}
#[command]
pub fn export_diagnostic_info<R: Runtime>(
app_handle: AppHandle<R>,
state: State<'_, RwLock<Platform>>,
) -> Result<(), Error> {
let info = state.inner().read().to_string();
let clipboard = app_handle.clipboard();
clipboard.write_text(info)
}
@@ -1,165 +0,0 @@
use std::{
fmt::{Debug, Display},
time::Instant,
};
pub mod commands;
#[cfg(windows)]
use deelevate::{PrivilegeLevel, Token};
use parking_lot::RwLock;
use sysinfo::{Networks, System};
use tauri::{
Manager as _, Runtime,
plugin::{Builder, TauriPlugin},
};
pub struct SysInfo {
system_name: String,
system_version: String,
system_kernel_version: String,
system_arch: String,
}
impl Default for SysInfo {
#[inline]
fn default() -> Self {
let system_name = System::name().unwrap_or_else(|| "Null".into());
let system_version = System::long_os_version().unwrap_or_else(|| "Null".into());
let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into());
let system_arch = System::cpu_arch();
Self {
system_name,
system_version,
system_kernel_version,
system_arch,
}
}
}
pub struct AppInfo {
app_version: String,
app_core_mode: String,
pub app_startup_time: Instant,
pub app_is_admin: bool,
}
impl Default for AppInfo {
#[inline]
fn default() -> Self {
let app_version = "0.0.0".into();
let app_core_mode = "NotRunning".into();
let app_is_admin = false;
let app_startup_time = Instant::now();
Self {
app_version,
app_core_mode,
app_startup_time,
app_is_admin,
}
}
}
#[derive(Default)]
pub struct Platform {
pub sysinfo: SysInfo,
pub appinfo: AppInfo,
}
impl Debug for Platform {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Platform")
.field("system_name", &self.sysinfo.system_name)
.field("system_version", &self.sysinfo.system_version)
.field("system_kernel_version", &self.sysinfo.system_kernel_version)
.field("system_arch", &self.sysinfo.system_arch)
.field("app_version", &self.appinfo.app_version)
.field("app_core_mode", &self.appinfo.app_core_mode)
.field("app_is_admin", &self.appinfo.app_is_admin)
.finish()
}
}
impl Display for Platform {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
self.sysinfo.system_name,
self.sysinfo.system_version,
self.sysinfo.system_kernel_version,
self.sysinfo.system_arch,
self.appinfo.app_version,
self.appinfo.app_core_mode,
self.appinfo.app_is_admin
)
}
}
impl Platform {
#[inline]
fn new() -> Self {
Self::default()
}
}
#[inline]
fn is_binary_admin() -> bool {
#[cfg(not(windows))]
unsafe {
libc::geteuid() == 0
}
#[cfg(windows)]
Token::with_current_process()
.and_then(|token| token.privilege_level())
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false)
}
#[inline]
pub fn list_network_interfaces() -> Vec<String> {
let mut networks = Networks::new();
networks.refresh(false);
networks.keys().map(|name| name.to_owned()).collect()
}
#[inline]
pub fn set_app_core_mode<R: Runtime>(app: &tauri::AppHandle<R>, mode: impl Into<String>) {
let platform_spec = app.state::<RwLock<Platform>>();
let mut spec = platform_spec.write();
spec.appinfo.app_core_mode = mode.into();
}
#[inline]
pub fn is_current_app_handle_admin<R: Runtime>(app: &tauri::AppHandle<R>) -> bool {
let platform_spec = app.state::<RwLock<Platform>>();
let spec = platform_spec.read();
spec.appinfo.app_is_admin
}
#[inline]
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::<R>::new("clash_verge_sysinfo")
// TODO 现在 crate 还不是真正的 tauri 插件,必须由主 lib 自行注册
// TODO 从 clash-verge 中迁移获取系统信息的 commnand 并实现优雅 structure.field 访问
// .invoke_handler(tauri::generate_handler![
// commands::get_system_info,
// commands::get_app_uptime,
// commands::app_is_admin,
// commands::export_diagnostic_info,
// ])
.setup(move |app, _api| {
let app_version = app.package_info().version.to_string();
let is_admin = is_binary_admin();
let mut platform_spec = Platform::new();
platform_spec.appinfo.app_version = app_version;
platform_spec.appinfo.app_is_admin = is_admin;
app.manage(RwLock::new(platform_spec));
Ok(())
})
.build()
}
+4
View File
@@ -0,0 +1,4 @@
files:
- source: /src/locales/en.json
translation: /src/locales
multilingual: 1
-79
View File
@@ -1,79 +0,0 @@
# CONTRIBUTING — i18n
Thanks for helping localize Clash Verge Rev. This guide reflects the current architecture, where the React frontend and the Tauri backend keep their translation bundles separate. Follow the steps below to keep both sides in sync without stepping on each other.
## Quick workflow
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
## Frontend locale structure
Each locale folder mirrors the namespaces under `src/locales/en/`:
```
src/locales/
en/
connections.json
home.json
shared.json
...
index.ts
zh/
...
```
- JSON files map to namespaces (for example `home.json` → `home.*`). Keep keys scoped to the file they belong to.
- `shared.json` stores reusable vocabulary (buttons, validations, etc.); feature-specific wording should live in the relevant namespace.
- `index.ts` re-exports a `resources` object that aggregates the namespace JSON files. When adding or removing namespaces, mirror the pattern from `src/locales/en/index.ts`.
- Frontend bundles are lazy-loaded by `src/services/i18n.ts`. Only languages listed in `supportedLanguages` are fetched at runtime, so append new codes there when you add a locale.
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
## Tooling for frontend contributors
- `pnpm format:i18n` → `node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English.
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
## Backend (Tauri) locale bundles
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `src-tauri/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
- Keep `en.yml` semantically aligned with the Simplified Chinese baseline (`zh.yml`). Other locales may temporarily copy English if no translation is available yet.
- When a backend feature introduces new strings, update every YAML file to keep the key set consistent. Missing keys fall back to the default language (`zh`), so catching gaps early avoids mixed-language output.
- Rust code resolves the active language through `src-tauri/src/utils/i18n.rs`. No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
## Adding a new language
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
2. Update the locale’s `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
4. If the backend should expose the language, create `src-tauri/locales/<new-lang>.yml` and translate the keys used in existing YAML files.
5. Adjust `crowdin.yml` if the locale requires a special mapping for Crowdin.
6. Run `pnpm format:i18n`, `pnpm i18n:types`, and (optionally) `pnpm node scripts/cleanup-unused-i18n.mjs` in dry-run mode to confirm structure.
## Authoring guidelines
- **Reuse shared vocabulary** before introducing new phrases—check `shared.json` for common actions, statuses, and labels.
- **Prefer semantic keys** (`systemProxy`, `updateInterval`, `autoRefresh`) over positional ones (`item1`, `dialogTitle2`).
- **Document placeholders** using `{{placeholder}}` and ensure components supply the required values.
- **Group keys by UI responsibility** inside each namespace (`page`, `sections`, `forms`, `actions`, `tooltips`, `notifications`, `errors`, `tables`, `statuses`, etc.).
- **Keep strings concise** to avoid layout issues. If a translation needs more context, leave a PR note so reviewers can verify the UI.
## Testing & QA
- Launch the desktop shell with `pnpm dev` (or `pnpm web:dev`) and navigate through the affected views to confirm translations load and layouts behave.
- Run `pnpm test` if you touched code that consumes translations or adjusts formatting logic.
- For backend changes, trigger the relevant tray actions or notifications to verify the updated copy.
- Note any remaining untranslated sections or layout concerns in your PR description so maintainers can follow up.
## Feedback & support
- File an issue for missing context, tooling bugs, or localization gaps so we can track them.
- PRs that touch UI should include screenshots or GIFs whenever text length may affect layout.
- Mention the commands you ran (formatting, type generation, tests) in the PR checklist. If you need extra context or review help, request it via a PR comment.
-125
View File
@@ -1,125 +0,0 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br>
</h1>
<h3 align="center">
A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
</h3>
<p align="center">
Languages:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p>
## Preview
| Dark | Light |
| ----------------------------------- | ------------------------------------- |
| ![Dark Preview](./preview_dark.png) | ![Light Preview](./preview_light.png) |
## Install
Visit the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the installer that matches your platform.<br>
We provide packages for Windows (x64/x86), Linux (x64/arm64), and macOS 10.15+ (Intel/Apple).
#### Choosing a Release Channel
| Channel | Description | Link |
| :---------- | :-------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | Official builds with high reliability, ideal for daily use. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | Legacy builds used to validate the publish pipeline. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | Rolling builds for testing and feedback. Expect experimental changes. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### Installation Guides & FAQ
Read the [project documentation](https://clash-verge-rev.github.io/) for install steps, troubleshooting, and frequently asked questions.
---
### Telegram Channel
Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
## Promotion
#### [Doggygo VPN — Performance-oriented global accelerator](https://verge.dginv.click/#/register?code=oaxsAGo6)
- High-performance overseas network service with free trials, discounted plans, streaming unlocks, and first-class Hysteria protocol support.
- Register through the exclusive Clash Verge link to get a 3-day trial with 1 GB of traffic per day: [Sign up](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Exclusive 20% off coupon for Clash Verge users: `verge20` (limited to 500 uses)
- Discounted bundle from ¥15.8 per month for 160 GB, plus an additional 20% off for yearly billing
- Operated by an overseas team with reliable service and up to 50% revenue share
- Load-balanced clusters with high-speed dedicated routes (compatible with legacy clients), exceptionally low latency, smooth 4K playback
- First global provider to support the `Hysteria2` protocol—perfect fit for the Clash Verge client
- Supports streaming services and ChatGPT access
- Official site: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
Our builds and releases run on YXVM dedicated servers that deliver premium resources, strong performance, and high-speed networking. If downloads feel fast and usage feels snappy, it is thanks to robust hardware.
🧩 Highlights of YXVM Dedicated Servers:
- 🌎 Optimized global routes for dramatically faster downloads
- 🔧 Bare-metal resources instead of shared VPS capacity for maximum performance
- 🧠 Great for proxy workloads, hosting web/CDN services, CI/CD pipelines, or any high-load tasks
- 💡 Ready to use instantly with multiple datacenter options, including CN2 and IEPL
- 📦 The configuration used by this project is on sale—feel free to get the same setup
- 🎯 Want the same build environment? [Order a YXVM server today](https://yxvm.com/aff.php?aff=827)
## Features
- Built on high-performance Rust with the Tauri 2 framework
- Ships with the embedded [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) core and supports switching to the `Alpha` channel
- Clean, polished UI with theme color controls, proxy group/tray icons, and `CSS Injection`
- Enhanced profile management (Merge and Script helpers) with configuration syntax hints
- System proxy controls, guard mode, and `TUN` (virtual network adapter) support
- Visual editors for nodes and rules
- WebDAV-based backup and sync for configurations
### FAQ
See the [FAQ page](https://clash-verge-rev.github.io/faq/windows.html) for platform-specific guidance.
### Donation
[Support Clash Verge Rev development](https://github.com/sponsors/clash-verge-rev)
## Development
See [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed contribution guidelines.
After installing all **Tauri** prerequisites, run the development shell with:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## Contributions
Issues and pull requests are welcome!
## Acknowledgement
Clash Verge Rev builds on or draws inspiration from these projects:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Tauri-based Clash GUI for Windows, macOS, and Linux.
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, more secure desktop apps with a web frontend.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel written in Go.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): A rule-based tunnel written in Go.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Clash GUI for Windows and macOS.
- [vitejs/vite](https://github.com/vitejs/vite): Next-generation frontend tooling with blazing-fast DX.
## License
GPL-3.0 License. See the [license file](../LICENSE) for details.
-125
View File
@@ -1,125 +0,0 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
Continuación de <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br>
</h1>
<h3 align="center">
Una interfaz gráfica para Clash Meta construida con <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
</h3>
<p align="center">
Idiomas:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p>
## Vista previa
| Oscuro | Claro |
| ----------------------------------- | ----------------------------------- |
| ![Vista oscura](./preview_dark.png) | ![Vista clara](./preview_light.png) |
## Instalación
Visita la [página de lanzamientos](https://github.com/clash-verge-rev/clash-verge-rev/releases) y descarga el instalador que corresponda a tu plataforma.<br>
Ofrecemos paquetes para Windows (x64/x86), Linux (x64/arm64) y macOS 10.15+ (Intel/Apple).
#### Cómo elegir el canal de lanzamiento
| Canal | Descripción | Enlace |
| :---------- | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | Compilaciones oficiales de alta fiabilidad; ideales para el uso diario. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | Compilaciones heredadas usadas para validar el flujo de publicación. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | Compilaciones continuas para pruebas y retroalimentación. Espera cambios beta. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### Guías de instalación y preguntas frecuentes
Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) para encontrar los pasos de instalación, solución de problemas y preguntas frecuentes.
---
### Canal de Telegram
Únete a [@clash_verge_rev](https://t.me/clash_verge_re) para enterarte de las novedades.
## Promociones
#### [Doggygo VPN — Acelerador global orientado al rendimiento](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Servicio internacional de alto rendimiento con prueba gratuita, planes con descuento, desbloqueo de streaming y soporte de protocolo Hysteria de primera clase.
- Regístrate mediante el enlace exclusivo de Clash Verge y obtén una prueba de 3 días con 1 GB de tráfico diario: [Regístrate](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Cupón exclusivo de 20% de descuento para usuarios de Clash Verge: `verge20` (limitado a 500 usos)
- Plan promocional desde ¥15.8 al mes con 160 GB, más 20% de descuento adicional por pago anual
- Equipo ubicado en el extranjero para un servicio confiable, con hasta 50% de comisión compartida
- Clústeres balanceados con rutas dedicadas de alta velocidad (compatibles con clientes antiguos), latencia extremadamente baja, reproducción 4K sin interrupciones
- Primer proveedor global que soporta el protocolo `Hysteria2`, ideal para el cliente Clash Verge
- Desbloquea servicios de streaming y acceso a ChatGPT
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Patrocinador de la infraestructura de compilación — [Servidores dedicados YXVM](https://yxvm.com/aff.php?aff=827)
Las compilaciones y lanzamientos del proyecto se ejecutan en servidores dedicados de YXVM, que proporcionan recursos premium, alto rendimiento y redes de alta velocidad. Si las descargas son rápidas y el uso es fluido, es gracias a este hardware robusto.
🧩 Ventajas de los servidores dedicados YXVM:
- 🌎 Rutas globales optimizadas para descargas significativamente más rápidas
- 🔧 Recursos bare-metal, en lugar de VPS compartidos, para obtener el máximo rendimiento
- 🧠 Ideales para proxys, alojamiento de sitios web/CDN, pipelines de CI/CD o cualquier carga elevada
- 💡 Listos para usar al instante, con múltiples centros de datos disponibles (incluidos CN2 e IEPL)
- 📦 La misma configuración utilizada por este proyecto está disponible para su compra
- 🎯 ¿Quieres el mismo entorno de compilación? [Solicita un servidor YXVM hoy](https://yxvm.com/aff.php?aff=827)
## Funciones
- Basado en Rust de alto rendimiento y en el framework Tauri 2
- Incluye el núcleo integrado [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) y permite cambiar al canal `Alpha`
- Interfaz limpia y elegante con controles de color de tema, iconos de grupos proxy/bandeja y `CSS Injection`
- Gestión avanzada de perfiles (herramientas Merge y Script) con sugerencias de sintaxis para configuraciones
- Control del proxy del sistema, modo guardián y soporte para `TUN` (adaptador de red virtual)
- Editores visuales para nodos y reglas
- Copias de seguridad y sincronización mediante WebDAV
### Preguntas frecuentes
Visita la [página de FAQ](https://clash-verge-rev.github.io/faq/windows.html) para obtener instrucciones específicas por plataforma.
### Donaciones
[Apoya el desarrollo de Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
## Desarrollo
Consulta [CONTRIBUTING.md](../CONTRIBUTING.md) para conocer las pautas de contribución.
Después de instalar todos los requisitos de **Tauri**, ejecuta el entorno de desarrollo con:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## Contribuciones
Se agradecen los issues y pull requests.
## Agradecimientos
Clash Verge Rev se basa en, o se inspira en, los siguientes proyectos:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Interfaz gráfica para Clash basada en Tauri. Compatible con Windows, macOS y Linux.
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Construye aplicaciones de escritorio más pequeñas, rápidas y seguras con un frontend web.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Túnel basado en reglas escrito en Go.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Túnel basado en reglas escrito en Go.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Interfaz de Clash para Windows y macOS.
- [vitejs/vite](https://github.com/vitejs/vite): Herramientas de frontend de nueva generación con una experiencia rapidísima.
## Licencia
Licencia GPL-3.0. Consulta el [archivo de licencia](../LICENSE) para más detalles.
-125
View File
@@ -1,125 +0,0 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
<a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a> の継続プロジェクト
<br>
</h1>
<h3 align="center">
<a href="https://github.com/tauri-apps/tauri">Tauri</a> で構築された Clash Meta GUI。
</h3>
<p align="center">
言語:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p>
## プレビュー
| ダーク | ライト |
| --------------------------------------- | ---------------------------------------- |
| ![ダークプレビュー](./preview_dark.png) | ![ライトプレビュー](./preview_light.png) |
## インストール
[リリースページ](https://github.com/clash-verge-rev/clash-verge-rev/releases) から、ご利用のプラットフォームに対応したインストーラーをダウンロードしてください。<br>
Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポートしています。
#### リリースチャンネルの選び方
| チャンネル | 説明 | リンク |
| :---------- | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 安定版。信頼性が高く、日常利用に最適です。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | 公開フローの検証に使用した旧テスト版。 | [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) |
#### インストール手順と FAQ
詳しい導入手順やトラブルシュートは [ドキュメントサイト](https://clash-verge-rev.github.io/) を参照してください。
---
### Telegram チャンネル
更新情報は [@clash_verge_rev](https://t.me/clash_verge_re) をフォローしてください。
## プロモーション
#### [Doggygo VPN — 高性能グローバルアクセラレータ](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 無料トライアル、割引プラン、ストリーミング解放、世界初の Hysteria プロトコル対応を備えた高性能海外ネットワークサービス。
- Clash Verge 専用リンクから登録すると、3 日間・1 日 1 GB の無料体験が利用できます。 [登録はこちら](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 利用者限定 20% オフクーポン: `verge20`(先着 500 名)
- 月額 15.8 元で 160 GB を利用できるプラン、年額契約ならさらに 20% オフ
- 海外チーム運営による高信頼サービス、収益シェアは最大 50%
- 負荷分散クラスタと高速専用回線(旧クライアント互換)、極低レイテンシで 4K も快適
- 世界初の `Hysteria2` プロトコル対応。Clash Verge クライアントとの相性抜群
- ストリーミングおよび ChatGPT の利用にも対応
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### ビルド環境スポンサー — [YXVM 専用サーバー](https://yxvm.com/aff.php?aff=827)
本プロジェクトのビルドとリリースは、YXVM の専用サーバーによって支えられています。高速ダウンロードや快適な操作性は、強力なハードウェアがあってこそです。
🧩 YXVM 専用サーバーの特長:
- 🌎 最適化されたグローバル回線で圧倒的なダウンロード速度
- 🔧 VPS とは異なるベアメタル資源で最高性能を発揮
- 🧠 プロキシ運用、Web/CDN ホスティング、CI/CD など高負荷ワークロードに最適
- 💡 複数データセンターから即時利用可能。CN2 や IEPL も選択可
- 📦 本プロジェクトが使用している構成も販売中。同じ環境を入手できます
- 🎯 同じビルド体験をしたい方は [今すぐ YXVM サーバーを注文](https://yxvm.com/aff.php?aff=827)
## 機能
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
- 組み込みの [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) コアを搭載し、`Alpha` チャンネルへの切り替えも可能
- テーマカラーやプロキシグループ/トレイアイコン、`CSS Injection` をカスタマイズできる洗練された UI
- 設定ファイルの管理および拡張(Merge・Script 支援)、構成シンタックスヒントを提供
- システムプロキシ制御、ガード機能、`TUN`(仮想ネットワークアダプタ)モード
- ノードとルールのビジュアルエディタ
- WebDAV による設定のバックアップと同期
### FAQ
プラットフォーム別の案内は [FAQ ページ](https://clash-verge-rev.github.io/faq/windows.html) を参照してください。
### 寄付
[Clash Verge Rev の開発を支援する](https://github.com/sponsors/clash-verge-rev)
## 開発
詳細な貢献ガイドは [CONTRIBUTING.md](../CONTRIBUTING.md) をご覧ください。
**Tauri** の前提条件を整えたら、以下のコマンドで開発サーバーを起動できます:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## コントリビューション
Issue や Pull Request を歓迎します。
## 謝辞
Clash Verge Rev は、以下のプロジェクトに影響を受けています。
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Tauri ベースの Clash GUI。Windows / macOS / Linux に対応。
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Web フロントエンドで小型・高速・安全なデスクトップアプリを構築するためのフレームワーク。
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Go 製のルールベーストンネル。
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Go 製のルールベーストンネル。
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Windows / macOS 向けの Clash GUI。
- [vitejs/vite](https://github.com/vitejs/vite): 次世代のフロントエンドツール群。高速な開発体験を提供。
## ライセンス
GPL-3.0 ライセンス。詳細は [LICENSE](../LICENSE) を参照してください。
-125
View File
@@ -1,125 +0,0 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
<a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>의 후속 프로젝트
<br>
</h1>
<h3 align="center">
<a href="https://github.com/tauri-apps/tauri">Tauri</a>로 제작된 Clash Meta GUI.
</h3>
<p align="center">
언어:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p>
## 미리보기
| 다크 | 라이트 |
| ------------------------------------ | --------------------------------------- |
| ![다크 미리보기](./preview_dark.png) | ![라이트 미리보기](./preview_light.png) |
## 설치
[릴리스 페이지](https://github.com/clash-verge-rev/clash-verge-rev/releases)에서 사용 중인 플랫폼에 맞는 설치 프로그램을 다운로드하세요.<br>
Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니다.
#### 릴리스 채널 선택
| 채널 | 설명 | 링크 |
| :---------- | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 안정 릴리스. 신뢰성이 높아 일상 사용에 적합합니다. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | 퍼블리시 파이프라인 검증에 사용되었던 구 테스트 채널입니다. | [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) |
#### 설치 가이드 및 FAQ
설치 방법, 트러블슈팅, 자주 묻는 질문은 [프로젝트 문서](https://clash-verge-rev.github.io/)를 참고하세요.
---
### 텔레그램 채널
업데이트 공지는 [@clash_verge_rev](https://t.me/clash_verge_re)에서 확인하세요.
## 프로모션
#### [Doggygo VPN — 고성능 글로벌 가속기](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 무료 체험, 할인 요금제, 스트리밍 해제, 선도적인 Hysteria 프로토콜 지원을 갖춘 고성능 해외 네트워크 서비스
- Clash Verge 전용 초대 링크로 가입 시 3일간 매일 1GB 무료 체험 제공: [가입하기](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 전용 20% 할인 코드: `verge20` (선착순 500회)
- 월 15.8위안부터 160GB 제공, 연간 결제 시 추가 20% 할인
- 해외 팀 운영, 높은 신뢰성, 최대 50% 커미션
- 로드밸런싱 클러스터, 고속 전용 회선(구 클라이언트 호환), 매우 낮은 지연, 4K도 쾌적
- 세계 최초 `Hysteria2` 프로토콜 지원 — Clash Verge 클라이언트와 최적의 궁합
- 스트리밍 및 ChatGPT 접근 지원
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
🧩 YXVM 전용 서버 하이라이트:
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
## 기능
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
- 내장 [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) 코어, `Alpha` 채널 전환 지원
- 테마 색상, 프록시 그룹/트레이 아이콘, `CSS Injection` 등 세련된 UI 커스터마이징
- 프로필 관리(병합 및 스크립트 보조), 구성 문법 힌트 제공
- 시스템 프록시 제어, 가드 모드, `TUN`(가상 네트워크 어댑터) 지원
- 노드/규칙 시각 편집기
- WebDAV 기반 설정 백업 및 동기화
### FAQ
플랫폼별 가이드는 [FAQ 페이지](https://clash-verge-rev.github.io/faq/windows.html)에서 확인하세요.
### 후원
[Clash Verge Rev 개발 후원](https://github.com/sponsors/clash-verge-rev)
## 개발
자세한 기여 가이드는 [CONTRIBUTING.md](../CONTRIBUTING.md)를 참고하세요.
**Tauri** 필수 구성 요소를 설치한 뒤 아래 명령으로 개발 서버를 실행합니다:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## 기여
Issue와 Pull Request를 환영합니다!
## 감사의 말
Clash Verge Rev는 다음 프로젝트에 기반하거나 영향을 받았습니다:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Windows / macOS / Linux용 Tauri 기반 Clash GUI
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): 웹 프론트엔드로 더 작고 빠르고 안전한 데스크톱 앱을 빌드
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Go로 작성된 규칙 기반 터널
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Go로 작성된 규칙 기반 터널
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Windows / macOS용 Clash GUI
- [vitejs/vite](https://github.com/vitejs/vite): 차세대 프론트엔드 툴링, 매우 빠른 DX
## 라이선스
GPL-3.0 라이선스. 자세한 내용은 [LICENSE](../LICENSE)를 참고하세요.
-121
View File
@@ -1,121 +0,0 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br>
</h1>
<h3 align="center">
Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
</h3>
<p align="center">
Языки:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p>
## Предпросмотр
| Тёмная тема | Светлая тема |
| ---------------------------------- | ------------------------------------ |
| ![Тёмная тема](./preview_dark.png) | ![Светлая тема](./preview_light.png) |
## Установка
Пожалуйста, перейдите на страницу релизов, чтобы скачать соответствующий установочный пакет: [Страница релизов](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
Перейти на [Страницу релизов](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
Поддержка Windows (x64/x86), Linux (x64/arm64) и macOS 10.15+ (intel/apple).
#### Как выбрать дистрибутив?
| Версия | Характеристики | Ссылка |
| :-------------------- | :------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- |
| Stable | Официальный релиз, высокая надежность, подходит для повседневного использования. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha(неиспользуемый) | Тестирование процесса публикации. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | Версия с постоянным обновлением, подходящая для тестирования и обратной связи. Может содержать дефекты. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### Инструкции по установке и ответы на часто задаваемые вопросы можно найти на [странице документации](https://clash-verge-rev.github.io/)
---
### TG канал: [@clash_verge_rev](https://t.me/clash_verge_re)
## Продвижение
#### [Doggygo VPN —— технический VPN-сервис (айрпорт)](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Высокопроизводительный иностранный VPN-сервис (айрпорт) с бесплатным пробным периодом, выгодными тарифами, возможностью разблокировки потокового ТВ и первым в мире поддержкой протокола Hysteria.
- Зарегистрируйтесь по эксклюзивной ссылке Clash Verge и получите 3 дня бесплатного использования, 1 Гб трафика в день: [регистрация](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Эксклюзивный промо-код на скидку 20% для Clash Verge: verge20 (только 500 штук)
- Специальный тарифный план всего за 15,8 юаней в месяц, 160 Гб трафика, скидка 20% при оплате за год
- Команда за рубежом, без риска побега, до 50% кэшбэка
- Архитектура с балансировкойнагрузки, высокоскоростная выделенная линия (совместима со старыми клиентами), чрезвычайно низкая задержка, без проблем в часы пик, 4K видео загружается мгновенно
- Первый в мире VPN-сервис (айрпорт), поддерживающий протокол Hysteria, теперь доступен более быстрый протокол `Hysteria2` (лучшее сочетание с клиентом Clash Verge)
- Разблокировка потоковые сервисы и ChatGPT
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Среда сборки и публикации этого проекта полностью поддерживается выделенным сервером [YXVM](https://yxvm.com/aff.php?aff=827)
Благодарим вас за предоставление надежной бэкэнд-среды с эксклюзивными ресурсами, высокой производительностью и высокоскоростной сетью. Если вы считаете, что загрузка файлов происходит достаточно быстро, а использование — достаточно плавно, то это потому, что мы используем серверы высшего уровня!
🧩 Преимущества выделенного сервера YXVM:
- 🌎 Премиум-сеть с оптимизацией обратного пути для молниеносной скорости загрузки
- 🔧 Выделенные физические серверные ресурсы, не имеющие аналогов среди VPS, обеспечивающие максимальную производительность
- 🧠 Идеально подходит для прокси, хостинга веб-сайтов/CDN-сайтов, рабочих процессов CI/CD или любых приложений с высокой нагрузкой
- 💡 Поддержка использования сразу после включения, выбор нескольких дата-центров, CN2 / IEPL на выбор
- 📦 Эта конфигурация в настоящее время доступна для покупки — не стесняйтесь заказывать ту же модель!
- 🎯 Хотите попробовать такую же сборку? [Закажите выделенный сервер YXVM прямо сейчас!](https://yxvm.com/aff.php?aff=827)
## Фичи
- Основан на произвоительном Rust и фреймворке Tauri 2
- Имеет встроенное ядро [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) и поддерживает переключение на ядро версии `Alpha`.
- Чистый и эстетичный пользовательский интерфейс, поддержка настраиваемых цветов темы, значков прокси-группы/системного трея и `CSS Injection`。
- Управление и расширение конфигурационными файлами (Merge и Script), подсказки по синтаксису конфигурационных файлов.
- Режим системного прокси и защита, `TUN (Tunneled Network Interface)` режим.
- Визуальное редактирование узлов и правил
- Резервное копирование и синхронизация конфигурации WebDAV
### FAQ
Смотрите [Страница часто задаваемых вопросов](https://clash-verge-rev.github.io/faq/windows.html)
### Донат
[Поддержите развитие Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
## Разработка
Дополнительные сведения смотреть в файле [CONTRIBUTING.md](../CONTRIBUTING.md).
Для запуска сервера разработки выполните следующие команды после установки всех необходимых компонентов для **Tauri**:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## Вклад
Обращения и запросы на PR приветствуются!
## Благодарность
Clash Verge rev был основан на этих проектах или вдохновлен ими, и так далее:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Графический интерфейс Clash на основе tauri. Поддерживает Windows, macOS и Linux.
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Создавайте более компактные, быстрые и безопасные настольные приложения с веб-интерфейсом.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Правило-ориентированный туннель на Go.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Правило-ориентированный туннель на Go.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Графический интерфейс пользователя для Windows/macOS на основе Clash.
- [vitejs/vite](https://github.com/vitejs/vite): Инструменты нового поколения для фронтенда. Они быстрые!
## Лицензия
GPL-3.0 License. Подробности смотрите в [Лицензии](../LICENSE).
+18 -58
View File
@@ -1,13 +1,12 @@
import eslintJS from "@eslint/js";
import eslintReact from "@eslint-react/eslint-plugin";
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import configPrettier from "eslint-config-prettier";
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
import pluginImportX from "eslint-plugin-import-x";
import pluginImport from "eslint-plugin-import";
import pluginPrettier from "eslint-plugin-prettier";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReactRefresh from "eslint-plugin-react-refresh";
import pluginUnusedImports from "eslint-plugin-unused-imports";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
@@ -16,20 +15,20 @@ export default defineConfig([
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: {
js: eslintJS,
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
js,
react: pluginReact,
"react-hooks": pluginReactHooks,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
"import-x": pluginImportX,
import: pluginImport,
"react-refresh": pluginReactRefresh,
"unused-imports": pluginUnusedImports,
prettier: pluginPrettier,
},
extends: [
eslintJS.configs.recommended,
"js/recommended",
tseslint.configs.recommended,
eslintReact.configs["recommended-typescript"],
pluginReact.configs.flat.recommended,
pluginReact.configs.flat["jsx-runtime"],
configPrettier,
],
@@ -41,11 +40,11 @@ export default defineConfig([
react: {
version: "detect",
},
"import-x/resolver-next": [
createTypeScriptImportResolver({
"import/resolver": {
typescript: {
project: "./tsconfig.json",
}),
],
},
},
},
rules: {
@@ -57,34 +56,6 @@ export default defineConfig([
{ allowConstantExport: true },
],
"@eslint-react/no-forward-ref": "off",
// React performance and production quality rules
"@eslint-react/no-array-index-key": "warn",
"@eslint-react/no-children-count": "error",
"@eslint-react/no-children-for-each": "error",
"@eslint-react/no-children-map": "error",
"@eslint-react/no-children-only": "error",
"@eslint-react/no-children-prop": "error",
"@eslint-react/no-children-to-array": "error",
"@eslint-react/no-class-component": "error",
"@eslint-react/no-clone-element": "error",
"@eslint-react/no-create-ref": "error",
"@eslint-react/no-default-props": "error",
"@eslint-react/no-direct-mutation-state": "error",
"@eslint-react/no-implicit-key": "error",
"@eslint-react/no-prop-types": "error",
"@eslint-react/no-set-state-in-component-did-mount": "error",
"@eslint-react/no-set-state-in-component-did-update": "error",
"@eslint-react/no-set-state-in-component-will-update": "error",
"@eslint-react/no-string-refs": "error",
"@eslint-react/no-unstable-context-value": "warn",
"@eslint-react/no-unstable-default-props": "warn",
"@eslint-react/no-unused-class-component-members": "error",
"@eslint-react/no-unused-state": "error",
"@eslint-react/no-useless-fragment": "warn",
"@eslint-react/prefer-destructuring-assignment": "warn",
// TypeScript
"@typescript-eslint/no-explicit-any": "off",
@@ -95,16 +66,15 @@ export default defineConfig([
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
varsIgnorePattern: "^_+$",
args: "after-used",
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^ignore",
argsIgnorePattern: "^_+$",
},
],
// Import
"import-x/no-unresolved": "error",
"import-x/order": [
"import/no-unresolved": "error",
"import/order": [
"warn",
{
groups: [
@@ -133,14 +103,4 @@ export default defineConfig([
"prettier/prettier": "warn",
},
},
{
files: ["scripts/**/*.{js,mjs,cjs}", "scripts-workflow/**/*.{js,mjs,cjs}"],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
]);
+60 -72
View File
@@ -1,9 +1,8 @@
{
"name": "clash-verge",
"version": "2.4.4-rc",
"version": "2.4.3",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
"dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
@@ -24,13 +23,11 @@
"release:autobuild": "pnpm release-version autobuild",
"release:deploytest": "pnpm release-version deploytest",
"publish-version": "node scripts/publish-version.mjs",
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --cache src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
"i18n:types": "node scripts/generate-i18n-keys.mjs",
"typecheck": "tsc --noEmit"
"format:check": "prettier --check ."
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -39,91 +36,82 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.6",
"@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.6",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tauri-apps/api": "2.9.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-http": "~2.5.4",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "2.3.3",
"@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.2",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0",
"ahooks": "^3.9.6",
"axios": "^1.13.2",
"dayjs": "1.11.19",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.5",
"axios": "^1.12.2",
"dayjs": "1.11.18",
"foxact": "^0.2.49",
"i18next": "^25.7.1",
"js-yaml": "^4.1.1",
"i18next": "^25.5.2",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.55.1",
"monaco-editor": "^0.53.0",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.6",
"react": "19.2.1",
"react-dom": "19.2.1",
"nanoid": "^5.1.5",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.67.0",
"react-i18next": "16.4.0",
"react-hook-form": "^7.62.0",
"react-i18next": "15.7.3",
"react-markdown": "10.1.0",
"react-router": "^7.10.0",
"react-virtuoso": "^4.16.1",
"swr": "^2.3.7",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main",
"types-pac": "^1.0.3"
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.9.1",
"react-virtuoso": "^4.14.0",
"swr": "^2.3.6",
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.3.12",
"@eslint/js": "^9.39.1",
"@tauri-apps/cli": "2.9.5",
"@eslint/js": "^9.35.0",
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react-swc": "^4.2.2",
"@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"commander": "^14.0.1",
"cross-env": "^10.0.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.0",
"globals": "^16.5.0",
"eslint-plugin-react": "^7.37.5",
"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",
"husky": "^9.1.7",
"jiti": "^2.6.1",
"lint-staged": "^16.2.7",
"jiti": "^2.5.1",
"meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2",
"prettier": "^3.7.4",
"sass": "^1.94.2",
"tar": "^7.5.2",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6",
"prettier": "^3.6.2",
"sass": "^1.92.1",
"tar": "^7.4.3",
"terser": "^5.44.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.0",
"vite": "^7.1.5",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
"*.{css,scss,json,md}": [
"prettier --write"
]
},
"type": "module",
"packageManager": "pnpm@10.22.0"
"packageManager": "pnpm@9.13.2"
}
+1534 -1848
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
onlyBuiltDependencies:
- "@parcel/watcher"
- "@swc/core"
- core-js
- es5-ext
- esbuild
- unrs-resolver
+42
View File
@@ -0,0 +1,42 @@
{
"extends": ["config:recommended", ":disableDependencyDashboard"],
"baseBranches": ["dev"],
"enabledManagers": ["cargo", "npm"],
"labels": ["dependencies"],
"ignorePaths": [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"**/crate/**",
"shared/**"
],
"rangeStrategy": "bump",
"packageRules": [
{
"semanticCommitType": "chore",
"matchPackageNames": ["*"]
},
{
"description": "Disable node/pnpm version updates",
"matchPackageNames": ["node", "pnpm"],
"matchDepTypes": ["engines", "packageManager"],
"enabled": false
},
{
"description": "Group all cargo dependencies into a single PR",
"matchManagers": ["cargo"],
"groupName": "cargo dependencies"
},
{
"description": "Group all npm dependencies into a single PR",
"matchManagers": ["npm"],
"groupName": "npm dependencies"
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["criterion"]
}
-50
View File
@@ -1,50 +0,0 @@
{
extends: ["config:recommended", ":disableDependencyDashboard"],
baseBranches: ["dev"],
enabledManagers: ["cargo", "npm", "github-actions"],
labels: ["dependencies"],
ignorePaths: [
"**/node_modules/**",
"**/bower_components/**",
"**/vendor/**",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/__fixtures__/**",
"shared/**",
],
rangeStrategy: "replace",
packageRules: [
{
matchUpdateTypes: ["patch"],
automerge: true,
},
{
semanticCommitType: "chore",
matchPackageNames: ["*"],
},
{
description: "Disable node/pnpm version updates",
matchPackageNames: ["node", "pnpm"],
matchDepTypes: ["engines", "packageManager"],
enabled: false,
},
{
description: "Group all cargo dependencies into a single PR",
matchManagers: ["cargo"],
groupName: "cargo dependencies",
},
{
description: "Group all npm dependencies into a single PR",
matchManagers: ["npm"],
groupName: "npm dependencies",
},
{
description: "Group all GitHub Actions updates into a single PR",
matchManagers: ["github-actions"],
groupName: "github actions",
},
],
postUpdateOptions: ["pnpmDedupe", "updateCargoLock"],
ignoreDeps: ["criterion"],
}
+102
View File
@@ -0,0 +1,102 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [
path.resolve(__dirname, "../src"),
path.resolve(__dirname, "../src-tauri"),
];
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
// 递归获取所有文件
function getAllFiles(dir, exts) {
let files = [];
fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts));
} else if (exts.includes(path.extname(full))) {
files.push(full);
}
});
return files;
}
// 读取所有源码内容为一个大字符串
function getAllSourceContent() {
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
}
// 白名单 key,不检查这些 key 是否被使用
const WHITELIST_KEYS = [
"theme.light",
"theme.dark",
"theme.system",
"Already Using Latest Core Version",
];
// 主流程
function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const keys = Object.keys(i18n);
const used = {};
const unused = [];
let checked = 0;
const total = keys.length;
keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key];
} else {
// 只查找一次
const regex = new RegExp(`["'\`]${key}["'\`]`);
if (regex.test(allSource)) {
used[key] = i18n[key];
} else {
unused.push(key);
}
}
checked++;
if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
}
});
// 输出未使用的 key
console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件
const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
}
function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs
.readdirSync(LOCALES_DIR)
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent();
files.forEach((file) => {
const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
});
}
main();
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env bash
#
# extract_update_logs.sh
# 从 Changelog.md 提取最新版本 (## v...) 的更新内容
# 并输出到屏幕或写入环境变量文件(如 GitHub Actions)
set -euo pipefail
CHANGELOG_FILE="Changelog.md"
if [[ ! -f "$CHANGELOG_FILE" ]]; then
echo "❌ 文件不存在: $CHANGELOG_FILE" >&2
exit 1
fi
# 提取从第一个 '## v' 开始到下一个 '## v' 前的内容
UPDATE_LOGS=$(awk '
/^## v/ {
if (found) exit;
found=1
}
found
' "$CHANGELOG_FILE")
if [[ -z "$UPDATE_LOGS" ]]; then
echo "⚠️ 未找到更新日志内容"
exit 0
fi
echo "✅ 提取到的最新版本日志内容如下:"
echo "----------------------------------------"
echo "$UPDATE_LOGS"
echo "----------------------------------------"
# 如果在 GitHub Actions 环境中(GITHUB_ENV 已定义)
if [[ -n "${GITHUB_ENV:-}" ]]; then
{
echo "UPDATE_LOGS<<EOF"
echo "$UPDATE_LOGS"
echo "EOF"
} >> "$GITHUB_ENV"
echo "✅ 已写入 GitHub 环境变量 UPDATE_LOGS"
fi
+1 -1
View File
@@ -1,7 +1,7 @@
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import { promisify } from "util";
/**
* 为Alpha版本重命名版本号
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, "..");
const LOCALE_DIR = path.resolve(ROOT_DIR, "src/locales/en");
const KEY_OUTPUT = path.resolve(ROOT_DIR, "src/types/generated/i18n-keys.ts");
const RESOURCE_OUTPUT = path.resolve(
ROOT_DIR,
"src/types/generated/i18n-resources.ts",
);
const isPlainObject = (value) =>
typeof value === "object" && value !== null && !Array.isArray(value);
const flattenKeys = (data, prefix = "") => {
const keys = [];
for (const [key, value] of Object.entries(data)) {
const nextPrefix = prefix ? `${prefix}.${key}` : key;
if (isPlainObject(value)) {
keys.push(...flattenKeys(value, nextPrefix));
} else {
keys.push(nextPrefix);
}
}
return keys;
};
const buildType = (data, indent = 0) => {
if (!isPlainObject(data)) {
return "string";
}
const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b));
const pad = " ".repeat(indent);
const inner = entries
.map(([key, value]) => {
const typeStr = buildType(value, indent + 2);
return `${" ".repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};`;
})
.join("\n");
return entries.length
? `{
${inner}
${pad}}`
: "{}";
};
const loadNamespaceJson = async () => {
const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true });
const namespaces = [];
for (const dirent of dirents) {
if (!dirent.isFile() || !dirent.name.endsWith(".json")) continue;
const name = dirent.name.replace(/\.json$/, "");
const filePath = path.join(LOCALE_DIR, dirent.name);
const raw = await fs.readFile(filePath, "utf8");
const json = JSON.parse(raw);
namespaces.push({ name, json });
}
namespaces.sort((a, b) => a.name.localeCompare(b.name));
return namespaces;
};
const buildKeysFile = (keys) => {
const arrayLiteral = keys.map((key) => ` "${key}"`).join(",\n");
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n`;
};
const buildResourcesFile = (namespaces) => {
const namespaceEntries = namespaces
.map(({ name, json }) => {
const typeStr = buildType(json, 4);
return ` ${JSON.stringify(name)}: ${typeStr};`;
})
.join("\n");
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n`;
};
const main = async () => {
const namespaces = await loadNamespaceJson();
const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name));
const keysContent = buildKeysFile(keys);
const resourcesContent = buildResourcesFile(namespaces);
await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true });
await fs.writeFile(KEY_OUTPUT, keysContent, "utf8");
await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, "utf8");
console.log(`Generated ${keys.length} translation keys.`);
};
main().catch((error) => {
console.error("Failed to generate i18n metadata:", error);
process.exitCode = 1;
});
+6 -7
View File
@@ -1,10 +1,9 @@
import fs from "fs";
import fsp from "fs/promises";
import { createRequire } from "module";
import path from "path";
import { context, getOctokit } from "@actions/github";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
@@ -50,9 +49,9 @@ async function resolvePortable() {
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
),
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
);
zip.addLocalFolder(configDir, ".config");
@@ -80,11 +79,11 @@ async function resolvePortable() {
tag,
});
const assets = release.assets.filter((x) => {
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
const id = assets[0].id;
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,
+2 -3
View File
@@ -1,9 +1,8 @@
import fs from "fs";
import fsp from "fs/promises";
import { createRequire } from "module";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import fsp from "fs/promises";
const target = process.argv.slice(2)[0];
const ARCH_MAP = {
+234 -385
View File
@@ -1,32 +1,18 @@
import { execSync } from "child_process";
import { createHash } from "crypto";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import zlib from "zlib";
import AdmZip from "adm-zip";
import { glob } from "glob";
import { HttpsProxyAgent } from "https-proxy-agent";
import fetch from "node-fetch";
import { extract } from "tar";
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
/**
* Prebuild script with optimization features:
* 1. Skip downloading mihomo core if it already exists (unless --force is used)
* 2. Cache version information for 1 hour to avoid repeated version checks
* 3. Use file hash to detect changes and skip unnecessary chmod/copy operations
* 4. Use --force or -f flag to force re-download and update all resources
*
*/
import path from "path";
import AdmZip from "adm-zip";
import fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";
import { execSync } from "child_process";
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
import { glob } from "glob";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
const VERSION_CACHE_FILE = path.join(TEMP_DIR, ".version_cache.json");
const HASH_CACHE_FILE = path.join(TEMP_DIR, ".hash_cache.json");
const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32",
@@ -57,7 +43,8 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
let target;
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
@@ -68,119 +55,18 @@ const SIDECAR_HOST = target
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0];
// =======================
// Version Cache
// =======================
async function loadVersionCache() {
try {
if (fs.existsSync(VERSION_CACHE_FILE)) {
const data = await fsp.readFile(VERSION_CACHE_FILE, "utf-8");
return JSON.parse(data);
}
} catch (err) {
log_debug("Failed to load version cache:", err.message);
}
return {};
}
async function saveVersionCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true });
await fsp.writeFile(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2));
log_debug("Version cache saved");
} catch (err) {
log_debug("Failed to save version cache:", err.message);
}
}
async function getCachedVersion(key) {
const cache = await loadVersionCache();
const cached = cache[key];
if (cached && Date.now() - cached.timestamp < 3600000) {
log_info(`Using cached version for ${key}: ${cached.version}`);
return cached.version;
}
return null;
}
async function setCachedVersion(key, version) {
const cache = await loadVersionCache();
cache[key] = { version, timestamp: Date.now() };
await saveVersionCache(cache);
}
// =======================
// Hash Cache & File Hash
// =======================
async function calculateFileHash(filePath) {
try {
const fileBuffer = await fsp.readFile(filePath);
const hashSum = createHash("sha256");
hashSum.update(fileBuffer);
return hashSum.digest("hex");
} catch (ignoreErr) {
return null;
}
}
async function loadHashCache() {
try {
if (fs.existsSync(HASH_CACHE_FILE)) {
const data = await fsp.readFile(HASH_CACHE_FILE, "utf-8");
return JSON.parse(data);
}
} catch (err) {
log_debug("Failed to load hash cache:", err.message);
}
return {};
}
async function saveHashCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true });
await fsp.writeFile(HASH_CACHE_FILE, JSON.stringify(cache, null, 2));
log_debug("Hash cache saved");
} catch (err) {
log_debug("Failed to save hash cache:", err.message);
}
}
async function hasFileChanged(filePath, targetPath) {
if (FORCE) return true;
if (!fs.existsSync(targetPath)) return true;
const hashCache = await loadHashCache();
const sourceHash = await calculateFileHash(filePath);
const targetHash = await calculateFileHash(targetPath);
if (!sourceHash || !targetHash) return true;
const cacheKey = targetPath;
const cachedHash = hashCache[cacheKey];
if (cachedHash === sourceHash && sourceHash === targetHash) {
return false;
}
return true;
}
async function updateHashCache(targetPath) {
const hashCache = await loadHashCache();
const hash = await calculateFileHash(targetPath);
if (hash) {
hashCache[targetPath] = hash;
await saveHashCache(hashCache);
}
}
// =======================
// Meta maps (stable & alpha)
// =======================
/* ======= clash meta alpha======= */
const META_ALPHA_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
let META_ALPHA_VERSION;
const META_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v1-go122",
"darwin-arm64": "mihomo-darwin-arm64-go122",
"darwin-x64": "mihomo-darwin-amd64-v1",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
@@ -189,116 +75,112 @@ const META_ALPHA_MAP = {
"linux-loong64": "mihomo-linux-loong64",
};
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2-go122",
"darwin-arm64": "mihomo-darwin-arm64-go122",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64",
};
// =======================
// Fetch latest versions
// =======================
// Fetch the latest alpha release version from the version.txt file
async function getLatestAlphaVersion() {
if (!FORCE) {
const cached = await getCachedVersion("META_ALPHA_VERSION");
if (cached) {
META_ALPHA_VERSION = cached;
return;
}
}
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
if (httpProxy) {
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
...options,
method: "GET",
});
if (!response.ok)
throw new Error(
`Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`,
);
META_ALPHA_VERSION = (await response.text()).trim();
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
await setCachedVersion("META_ALPHA_VERSION", META_ALPHA_VERSION);
} catch (err) {
log_error("Error fetching latest alpha version:", err.message);
} catch (error) {
log_error("Error fetching latest alpha version:", error.message);
process.exit(1);
}
}
/* ======= clash meta stable ======= */
const META_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION;
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest release version from the version.txt file
async function getLatestReleaseVersion() {
if (!FORCE) {
const cached = await getCachedVersion("META_VERSION");
if (cached) {
META_VERSION = cached;
return;
}
}
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
if (httpProxy) {
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_VERSION_URL, {
...options,
method: "GET",
});
if (!response.ok)
throw new Error(
`Failed to fetch ${META_VERSION_URL}: ${response.status}`,
);
META_VERSION = (await response.text()).trim();
let v = await response.text();
META_VERSION = v.trim(); // Trim to remove extra whitespaces
log_info(`Latest release version: ${META_VERSION}`);
await setCachedVersion("META_VERSION", META_VERSION);
} catch (err) {
log_error("Error fetching latest release version:", err.message);
} catch (error) {
log_error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
// =======================
// Validate availability
// =======================
/*
* check available
*/
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
// =======================
// Build meta objects
// =======================
/**
* core info
*/
function clashMetaAlpha() {
const name = META_ALPHA_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
return {
name: "verge-mihomo-alpha",
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile: `${name}${isWin ? ".exe" : ""}`,
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
exeFile,
zipFile,
downloadURL,
};
}
@@ -306,83 +188,35 @@ function clashMeta() {
const name = META_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
return {
name: "verge-mihomo",
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile: `${name}${isWin ? ".exe" : ""}`,
zipFile: `${name}-${META_VERSION}.${urlExt}`,
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
exeFile,
zipFile,
downloadURL,
};
}
// =======================
// download helper (增强:status + magic bytes)
// =======================
async function downloadFile(url, outPath) {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
const response = await fetch(url, {
...options,
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
if (!response.ok) {
const body = await response.text().catch(() => "");
// 将 body 写到文件以便排查(可通过临时目录查看)
await fsp.mkdir(path.dirname(outPath), { recursive: true });
await fsp.writeFile(outPath, body);
throw new Error(`Failed to download ${url}: status ${response.status}`);
}
const buf = Buffer.from(await response.arrayBuffer());
await fsp.mkdir(path.dirname(outPath), { recursive: true });
// 简单 magic 字节检查
if (url.endsWith(".gz") || url.endsWith(".tgz")) {
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
await fsp.writeFile(outPath, buf);
throw new Error(
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
);
}
} else if (url.endsWith(".zip")) {
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
await fsp.writeFile(outPath, buf);
throw new Error(
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
);
}
}
await fsp.writeFile(outPath, buf);
log_success(`download finished: ${url}`);
}
// =======================
// resolveSidecar (支持 zip / tgz / gz)
// =======================
/**
* download sidecar and rename
*/
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
const sidecarPath = path.join(sidecarDir, targetFile);
await fsp.mkdir(sidecarDir, { recursive: true });
if (!FORCE && fs.existsSync(sidecarPath)) {
log_success(`"${name}" already exists, skipping download`);
return;
}
await fsp.mkdir(sidecarDir, { recursive: true });
if (!FORCE && fs.existsSync(sidecarPath)) return;
const tempDir = path.join(TEMP_DIR, name);
const tempZip = path.join(tempDir, zipFile);
const tempExe = path.join(tempDir, exeFile);
await fsp.mkdir(tempDir, { recursive: true });
await fsp.mkdir(tempDir, { recursive: true });
try {
if (!fs.existsSync(tempZip)) {
await downloadFile(downloadURL, tempZip);
@@ -391,118 +225,140 @@ async function resolveSidecar(binInfo) {
if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
log_debug(`"${name}" entry: ${entry.entryName}`);
log_debug(`"${name}" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
// 尝试按 exeFile 重命名,否则找第一个可执行文件
if (fs.existsSync(tempExe)) {
await fsp.rename(tempExe, sidecarPath);
} else {
// 搜索候选
const files = await fsp.readdir(tempDir);
const candidate = files.find(
(f) =>
f === path.basename(exeFile) ||
f.endsWith(".exe") ||
!f.includes("."),
);
if (!candidate)
throw new Error(`Expected binary not found in ${tempDir}`);
await fsp.rename(path.join(tempDir, candidate), sidecarPath);
}
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
await fsp.rename(tempExe, sidecarPath);
log_success(`unzip finished: "${name}"`);
} else if (zipFile.endsWith(".tgz")) {
await extract({ cwd: tempDir, file: tempZip });
// tgz
await fsp.mkdir(tempDir, { recursive: true });
await extract({
cwd: tempDir,
file: tempZip,
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
});
const files = await fsp.readdir(tempDir);
log_debug(`"${name}" extracted files:`, files);
// 优先寻找给定 exeFile 或已知前缀
let extracted = files.find(
(f) =>
f === path.basename(exeFile) ||
f.startsWith("虚空终端-") ||
!f.includes("."),
);
if (!extracted) extracted = files[0];
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`);
await fsp.rename(path.join(tempDir, extracted), sidecarPath);
execSync(`chmod 755 ${sidecarPath}`);
log_success(`tgz processed: "${name}"`);
log_debug(`"${name}" files in tempDir:`, files);
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
if (extractedFile) {
const extractedFilePath = path.join(tempDir, extractedFile);
await fsp.rename(extractedFilePath, sidecarPath);
log_success(`"${name}" file renamed to "${sidecarPath}"`);
execSync(`chmod 755 ${sidecarPath}`);
log_success(`chmod binary finished: "${name}"`);
} else {
throw new Error(`Expected file not found in ${tempDir}`);
}
} else {
// .gz
// gz
const readStream = fs.createReadStream(tempZip);
const writeStream = fs.createWriteStream(sidecarPath);
await new Promise((resolve, reject) => {
const onError = (error) => {
log_error(`"${name}" gz failed:`, error.message);
reject(error);
};
readStream
.pipe(zlib.createGunzip())
.on("error", (e) => {
log_error(`gunzip error for ${name}:`, e.message);
reject(e);
})
.pipe(zlib.createGunzip().on("error", onError))
.pipe(writeStream)
.on("finish", () => {
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
execSync(`chmod 755 ${sidecarPath}`);
log_success(`chmod binary finished: "${name}"`);
resolve();
})
.on("error", (e) => {
log_error(`write stream error for ${name}:`, e.message);
reject(e);
});
.on("error", onError);
});
log_success(`gz binary processed: "${name}"`);
}
} catch (err) {
// 需要删除文件
await fsp.rm(sidecarPath, { recursive: true, force: true });
throw err;
} finally {
// delete temp dir
await fsp.rm(tempDir, { recursive: true, force: true });
}
}
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
localPath: path.join(cwd, "scripts/set_dns.sh"),
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
localPath: path.join(cwd, "scripts/unset_dns.sh"),
});
/**
* download the file to the resources dir
*/
async function resolveResource(binInfo) {
const { file, downloadURL, localPath } = binInfo;
const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
if (!FORCE && fs.existsSync(targetPath) && !downloadURL && !localPath) {
log_success(`"${file}" already exists, skipping`);
return;
}
if (!FORCE && fs.existsSync(targetPath)) return;
if (downloadURL) {
if (!FORCE && fs.existsSync(targetPath)) {
log_success(`"${file}" already exists, skipping download`);
return;
}
await fsp.mkdir(resDir, { recursive: true });
await downloadFile(downloadURL, targetPath);
await updateHashCache(targetPath);
}
if (localPath) {
if (!(await hasFileChanged(localPath, targetPath))) {
return;
}
await fsp.mkdir(resDir, { recursive: true });
await fsp.copyFile(localPath, targetPath);
await updateHashCache(targetPath);
log_success(`Copied file: ${file}`);
await fs.copyFile(localPath, targetPath, (err) => {
if (err) {
console.error("Error copying file:", err);
} else {
console.log("File was copied successfully");
}
});
log_debug(`copy file finished: "${localPath}"`);
}
log_success(`${file} finished`);
}
// SimpleSC.dll (win plugin)
/**
* download file and save to `path`
*/ async function downloadFile(url, path) {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = new HttpsProxyAgent(httpProxy);
}
const response = await fetch(url, {
...options,
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
const buffer = await response.arrayBuffer();
await fsp.writeFile(path, new Uint8Array(buffer));
log_success(`download finished: ${url}`);
}
// SimpleSC.dll
const resolvePlugin = async () => {
const url =
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA || "", "Local/NSIS");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fsp.mkdir(pluginDir, { recursive: true });
await fsp.mkdir(tempDir, { recursive: true });
@@ -512,95 +368,95 @@ const resolvePlugin = async () => {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip
.getEntries()
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName));
zip.getEntries().forEach((entry) => {
log_debug(`"SimpleSC" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
if (fs.existsSync(tempDll)) {
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
log_success(`unzip finished: "SimpleSC"`);
} else {
// 如果 dll 名称不同,尝试找到 dll
const files = await fsp.readdir(tempDir);
const dll = files.find((f) => f.toLowerCase().endsWith(".dll"));
if (dll) {
await fsp.cp(path.join(tempDir, dll), pluginPath, {
recursive: true,
force: true,
});
log_success(`unzip finished: "SimpleSC" (found ${dll})`);
} else {
throw new Error("SimpleSC.dll not found in zip");
}
}
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
log_success(`unzip finished: "SimpleSC"`);
} finally {
await fsp.rm(tempDir, { recursive: true, force: true });
}
};
// service chmod (保留并使用 glob)
// service chmod
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service*",
"clash-verge-service-install*",
"clash-verge-service-uninstall*",
"install-service*",
"uninstall-service*",
];
const resDir = path.join(cwd, "src-tauri/resources");
const hashCache = await loadHashCache();
let hasChanges = false;
for (const f of serviceExecutables) {
for (let f of serviceExecutables) {
// 使用glob模块来处理通配符
const files = glob.sync(path.join(resDir, f));
for (const filePath of files) {
for (let filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath);
const cacheKey = `${filePath}_chmod`;
if (!FORCE && hashCache[cacheKey] === currentHash) {
continue;
}
try {
execSync(`chmod 755 ${filePath}`);
log_success(`chmod finished: "${filePath}"`);
} catch (e) {
log_error(`chmod failed for ${filePath}:`, e.message);
}
hashCache[cacheKey] = currentHash;
hasChanges = true;
execSync(`chmod 755 ${filePath}`);
log_success(`chmod finished: "${filePath}"`);
}
}
}
if (hasChanges) {
await saveHashCache(hashCache);
}
};
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
// 在 resolveResource 函数后添加新函数
async function resolveLocales() {
const srcLocalesDir = path.join(cwd, "src/locales");
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
try {
// 确保目标目录存在
await fsp.mkdir(targetLocalesDir, { recursive: true });
// 读取所有语言文件
const files = await fsp.readdir(srcLocalesDir);
// 复制每个文件
for (const file of files) {
const srcPath = path.join(srcLocalesDir, file);
const targetPath = path.join(targetLocalesDir, file);
await fsp.copyFile(srcPath, targetPath);
log_success(`Copied locale file: ${file}`);
}
log_success("All locale files copied successfully");
} catch (err) {
log_error("Error copying locale files:", err.message);
throw err;
}
}
/**
* main
*/
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "clash-verge-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
const resolveInstall = () => {
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-install" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "install-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/install-service${ext}`,
});
};
const resolveUninstall = () => {
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-uninstall" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "uninstall-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
});
};
@@ -624,27 +480,15 @@ const resolveEnableLoopback = () =>
file: "enableLoopback.exe",
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const resolveWinSysproxy = () =>
resolveResource({
file: "sysproxy.exe",
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
});
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
localPath: path.join(cwd, "scripts/set_dns.sh"),
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
localPath: path.join(cwd, "scripts/unset_dns.sh"),
});
// =======================
// Tasks
// =======================
const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
name: "verge-mihomo-alpha",
func: () =>
@@ -694,6 +538,11 @@ const tasks = [
retry: 5,
macosOnly: true,
},
{
name: "locales",
func: resolveLocales,
retry: 2,
},
];
async function runTask() {
+10 -21
View File
@@ -30,11 +30,10 @@
*/
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
/**
* 获取当前 git 短 commit hash
* @returns {string}
@@ -59,16 +58,11 @@ function getLatestTauriCommit() {
)
.toString()
.trim();
const shortHash = execSync(`git rev-parse --short ${fullHash}`)
.toString()
.trim();
console.log(`[INFO]: Latest Tauri-related commit: ${shortHash}`);
return shortHash;
} catch (error) {
return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
} catch {
console.warn(
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
);
console.warn(`[WARN]: Error details: ${error.message}`);
return getGitShortCommit();
}
}
@@ -77,10 +71,9 @@ function getLatestTauriCommit() {
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
* 使用 Asia/Shanghai 时区
* @param {boolean} withCommit 是否带 commit
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效)
* @returns {string}
*/
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
function generateShortTimestamp(withCommit = false) {
const now = new Date();
const formatter = new Intl.DateTimeFormat("en-CA", {
@@ -94,9 +87,7 @@ function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
const day = parts.find((part) => part.type === "day").value;
if (withCommit) {
const gitShort = useTauriCommit
? getLatestTauriCommit()
: getGitShortCommit();
const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`;
}
return `${month}${day}`;
@@ -274,17 +265,15 @@ async function main(versionArg) {
const baseVersion = getBaseVersion(currentVersion);
if (versionArg.toLowerCase() === "autobuild") {
// 格式: 2.3.0+autobuild.1004.cc39b27
// 使用 Tauri 相关的最新 commit hash
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
// 格式: 2.3.0+autobuild.250613.cc39b27
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
} else if (versionArg.toLowerCase() === "autobuild-latest") {
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
// 格式: 2.3.0+autobuild.0614.a1b2c3d (使用最新 Tauri 提交)
const latestTauriCommit = getLatestTauriCommit();
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
} else if (versionArg.toLowerCase() === "deploytest") {
// 格式: 2.3.0+deploytest.1004.cc39b27
// 使用 Tauri 相关的最新 commit hash
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
// 格式: 2.3.0+deploytest.250613.cc39b27
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true)}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}
+2 -17
View File
@@ -1,8 +1,6 @@
import { readFileSync } from "fs";
import axios from "axios";
import { log_error, log_info, log_success } from "./utils.mjs";
import { readFileSync } from "fs";
import { log_success, log_error, log_info } from "./utils.mjs";
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
const CHAT_ID_TEST = "@vergetest"; // 测试频道
@@ -73,19 +71,6 @@ async function sendTelegramNotification() {
.join("\n");
}
function normalizeDetailsTags(content) {
return content
.replace(
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
"\n<b>$1</b>\n",
)
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
.replace(/<\/?details>/g, "")
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
.replace(/<br\s*\/?>/g, "\n");
}
releaseContent = normalizeDetailsTags(releaseContent);
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
+7 -7
View File
@@ -2,9 +2,9 @@ import fs from "fs";
import fsp from "fs/promises";
import path from "path";
const UPDATE_LOG = "Changelog.md";
const UPDATE_LOG = "UPDATELOG.md";
// parse the Changelog.md
// parse the UPDATELOG.md
export async function resolveUpdateLog(tag) {
const cwd = process.cwd();
@@ -14,7 +14,7 @@ export async function resolveUpdateLog(tag) {
const file = path.join(cwd, UPDATE_LOG);
if (!fs.existsSync(file)) {
throw new Error("could not found Changelog.md");
throw new Error("could not found UPDATELOG.md");
}
const data = await fsp.readFile(file, "utf-8");
@@ -38,7 +38,7 @@ export async function resolveUpdateLog(tag) {
});
if (!map[tag]) {
throw new Error(`could not found "${tag}" in Changelog.md`);
throw new Error(`could not found "${tag}" in UPDATELOG.md`);
}
return map[tag].join("\n").trim();
@@ -49,7 +49,7 @@ export async function resolveUpdateLogDefault() {
const file = path.join(cwd, UPDATE_LOG);
if (!fs.existsSync(file)) {
throw new Error("could not found Changelog.md");
throw new Error("could not found UPDATELOG.md");
}
const data = await fsp.readFile(file, "utf-8");
@@ -58,7 +58,7 @@ export async function resolveUpdateLogDefault() {
const reEnd = /^---/;
let isCapturing = false;
const content = [];
let content = [];
let firstTag = "";
for (const line of data.split("\n")) {
@@ -77,7 +77,7 @@ export async function resolveUpdateLogDefault() {
}
if (!firstTag) {
throw new Error("could not found any version tag in Changelog.md");
throw new Error("could not found any version tag in UPDATELOG.md");
}
return content.join("\n").trim();
+3 -4
View File
@@ -1,6 +1,5 @@
import { context, getOctokit } from "@actions/github";
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";
const UPDATE_TAG_NAME = "updater";
@@ -36,7 +35,7 @@ async function resolveUpdater() {
const updateData = {
name: tag.name,
notes: await resolveUpdateLog(tag.name), // use Changelog.md
notes: await resolveUpdateLog(tag.name), // use updatelog.md
pub_date: new Date().toISOString(),
platforms: {
"windows-x86_64": { signature: "", url: "" },
@@ -114,7 +113,7 @@ async function resolveUpdater() {
});
// delete the old assets
for (const asset of updateRelease.assets) {
for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,
+2 -3
View File
@@ -1,6 +1,5 @@
import { getOctokit, context } from "@actions/github";
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
// Add stable update JSON filenames
@@ -260,7 +259,7 @@ async function processRelease(github, options, tag, isAlpha) {
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
// Delete existing assets with these names
for (const asset of updateRelease.assets) {
for (let asset of updateRelease.assets) {
if (asset.name === jsonFile) {
await github.rest.repos.deleteReleaseAsset({
...options,
+1 -2
View File
@@ -1,2 +1 @@
avoid-breaking-exported-api = true
cognitive-complexity-threshold = 25
avoid-breaking-exported-api = true
+1358 -1940
View File
File diff suppressed because it is too large Load Diff
+156 -78
View File
@@ -1,109 +1,96 @@
[package]
name = "clash-verge"
version = "2.4.4-rc"
version = "2.4.3"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
edition = "2024"
build = "build.rs"
edition = { workspace = true }
rust-version = { workspace = true }
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = ["clash_verge_logger/color"]
tauri-dev = ["clash-verge-logging/tauri-dev"]
tokio-trace = ["console-subscriber"]
clippy = ["tauri/test"]
tracing = []
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.5.3", features = [] }
tauri-build = { version = "2.4.1", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
clash-verge-logging = { workspace = true }
clash-verge-signal = { workspace = true }
clash-verge-types = { workspace = true }
tauri-plugin-clash-verge-sysinfo = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
tauri = { workspace = true, features = [
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.99"
dirs = "6.0"
open = "5.3.2"
log = "0.4.28"
dunce = "1.0.5"
nanoid = "0.4"
chrono = "0.4.42"
sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
once_cell = "1.21.3"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
parking_lot = "0.12.4"
percent-encoding = "2.3.2"
tokio = { version = "1.47.1", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
serde = { version = "1.0.225", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.5", features = [
"protocol-asset",
"devtools",
"tray-icon",
"image-ico",
"image-png",
] }
parking_lot = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true }
compact_str = { workspace = true }
flexi_logger = { workspace = true }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
warp = { version = "0.4.2", features = ["server"] }
open = "5.3.3"
dunce = "1.0.5"
nanoid = "0.4"
chrono = "0.4.42"
boa_engine = "0.21.0"
once_cell = { version = "1.21.3", features = ["parking_lot"] }
port_scanner = "0.1.5"
delay_timer = "0.11.6"
percent-encoding = "2.3.2"
reqwest = { version = "0.12.24", features = ["json", "cookies", "rustls-tls"] }
regex = "1.12.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
"guard",
] }
network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.3"
tauri-plugin-dialog = "2.4.2"
tauri-plugin-fs = "2.4.4"
tauri-plugin-process = "2.3.1"
tauri-plugin-deep-link = "2.4.5"
tauri-plugin-window-state = "2.4.1"
zip = "6.0.0"
tauri-plugin-shell = "2.3.1"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-fs = "2.4.2"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-window-state = "2.4.0"
zip = "5.1.1"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.4"
getrandom = "0.3.3"
futures = "0.3.31"
sys-locale = "0.3.2"
gethostname = "1.1.0"
libc = "0.2.175"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.3"
kode-bridge = "0.2.1"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
"parking_lot",
] }
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2.5.4"
console-subscriber = { version = "0.5.0", optional = true }
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" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
clash_verge_service_ipc = { version = "2.0.24", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
rust-i18n = "3.1.5"
rust_iso3166 = "0.1.14"
dark-light = "2.0.0"
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.55.0"
winapi = { version = "0.3.9", features = [
"winbase",
@@ -119,13 +106,104 @@ winapi = { version = "0.3.9", features = [
"winreg",
] }
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1"
tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-updater = "2.9.0"
[dev-dependencies]
criterion = { workspace = true }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = ["nu-ansi-term"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
[lints]
workspace = true
[profile.release]
panic = "abort"
codegen-units = 16
lto = true
opt-level = 2
debug = false
strip = true
overflow-checks = false
rpath = false
[profile.dev]
incremental = true
codegen-units = 64
opt-level = 0
debug = true
strip = "none"
overflow-checks = true
lto = false
rpath = false
[profile.fast-release]
inherits = "release"
incremental = true
codegen-units = 64
lto = false
opt-level = 0
debug = true
strip = false
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
criterion = "0.7.0"
[lints.clippy]
# Core categories - most important for code safety and correctness
correctness = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }
# Critical safety lints - warn for now due to extensive existing usage
unwrap_used = "warn"
expect_used = "warn"
panic = "deny"
unimplemented = "deny"
# Development quality lints
todo = "warn"
dbg_macro = "warn"
#print_stdout = "warn"
#print_stderr = "warn"
# Performance lints for proxy application
clone_on_ref_ptr = "warn"
rc_clone_in_vec_init = "warn"
large_stack_arrays = "warn"
large_const_arrays = "warn"
# Security lints
#integer_division = "warn"
#lossy_float_literal = "warn"
#default_numeric_fallback = "warn"
# Mutex and async lints - strict control
async_yields_async = "deny" # Prevents missing await in async blocks
mutex_atomic = "deny" # Use atomics instead of Mutex<bool/int>
mutex_integer = "deny" # Use AtomicInt instead of Mutex<int>
rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong
unused_async = "deny" # Too many false positives in Tauri/framework code
await_holding_lock = "deny"
large_futures = "deny"
future_not_send = "deny"
# Common style improvements
redundant_else = "deny" # Too many in existing code
needless_continue = "deny" # Too many in existing code
needless_raw_string_hashes = "deny" # Too many in existing code
# Disable noisy categories for existing codebase but keep them available
#style = { level = "allow", priority = -1 }
#complexity = { level = "allow", priority = -1 }
#perf = { level = "allow", priority = -1 }
#pedantic = { level = "allow", priority = -1 }
#nursery = { level = "allow", priority = -1 }
#restriction = { level = "allow", priority = -1 }
+91
View File
@@ -0,0 +1,91 @@
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;
// 业务模型 & Draft
use app_lib::config::Draft as DraftNew;
use app_lib::config::IVerge;
// fn bench_apply_old(c: &mut Criterion) {
// c.bench_function("apply_draft_old", |b| {
// b.iter(|| {
// let verge = Box::new(IVerge {
// enable_auto_launch: Some(true),
// enable_tun_mode: Some(false),
// ..Default::default()
// });
// let draft = DraftOld::from(black_box(verge));
// {
// let mut d = draft.draft_mut();
// d.enable_auto_launch = Some(false);
// }
// let _ = draft.apply();
// });
// });
// }
// fn bench_discard_old(c: &mut Criterion) {
// c.bench_function("discard_draft_old", |b| {
// b.iter(|| {
// let verge = Box::new(IVerge::default());
// let draft = DraftOld::from(black_box(verge));
// {
// let mut d = draft.draft_mut();
// d.enable_auto_launch = Some(false);
// }
// let _ = draft.discard();
// });
// });
// }
/// 基准:修改草稿并 apply()
fn bench_apply_new(c: &mut Criterion) {
c.bench_function("apply_draft_new", |b| {
b.iter(|| {
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..Default::default()
});
let draft = DraftNew::from(black_box(verge));
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.apply();
});
});
}
/// 基准:修改草稿并 discard()
fn bench_discard_new(c: &mut Criterion) {
c.bench_function("discard_draft_new", |b| {
b.iter(|| {
let verge = Box::new(IVerge::default());
let draft = DraftNew::from(black_box(verge));
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.discard();
});
});
}
criterion_group!(
benches,
// bench_apply_old,
// bench_discard_old,
bench_apply_new,
bench_discard_new
);
criterion_main!(benches);
+1 -7
View File
@@ -1,9 +1,3 @@
fn main() {
#[cfg(feature = "clippy")]
{
println!("cargo:warning=Skipping tauri_build during Clippy");
}
#[cfg(not(feature = "clippy"))]
tauri_build::build();
tauri_build::build()
}
+1 -2
View File
@@ -24,7 +24,6 @@
{
"identifier": "http:default",
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
},
"mihomo:default"
}
]
}
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Regel
direct: Direkt
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-61
View File
@@ -1,61 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Installing the service requires administrator privileges.
adminUninstallPrompt: Uninstalling the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Regla
direct: Directo
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Aturan
direct: Langsung
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: ルール
direct: ダイレクト
global: グローバル
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: 대시보드
body: 대시보드 표시 상태가 업데이트되었습니다.
clashModeChanged:
title: 모드 전환
body: "{mode}(으)로 전환되었습니다."
systemProxyToggled:
title: 시스템 프록시
body: 시스템 프록시 상태가 업데이트되었습니다.
tunModeToggled:
title: TUN 모드
body: TUN 모드 상태가 업데이트되었습니다.
lightweightModeEntered:
title: 경량 모드
body: 경량 모드에 진입했습니다.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: 곧 종료
body: Clash Verge가 곧 종료됩니다.
appHidden:
title: 앱이 숨겨짐
body: Clash Verge가 백그라운드에서 실행 중입니다.
service:
adminPrompt: 서비스를 설치하려면 관리자 권한이 필요합니다.
tray:
dashboard: 대시보드
ruleMode: 규칙 모드
globalMode: 전역 모드
directMode: 직접 모드
outboundModes: Outbound Modes
rule: 규칙
direct: 직접
global: 글로벌
profiles: 프로필
proxies: 프록시
systemProxy: 시스템 프록시
tunMode: TUN 모드
closeAllConnections: 모든 연결 닫기
lightweightMode: 경량 모드
copyEnv: 환경 변수 복사
confDir: 구성 디렉터리
coreDir: 코어 디렉터리
logsDir: 로그 디렉터리
openDir: 디렉터리 열기
appLog: 애플리케이션 로그
coreLog: 코어 로그
restartClash: Clash 코어 재시작
restartApp: 애플리케이션 재시작
vergeVersion: Verge 버전
more: 더 보기
exit: 종료
tooltip:
systemProxy: 시스템 프록시
tun: TUN
profile: 프로필
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Правило
direct: Прямой
global: Глобальный
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Kural
direct: Doğrudan
global: Küresel
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-59
View File
@@ -1,59 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiles
body: Profile Reactivated.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile
-60
View File
@@ -1,60 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: 仪表板
body: 仪表板显示状态已更新。
clashModeChanged:
title: 模式切换
body: 已切换至 {mode}。
systemProxyToggled:
title: 系统代理
body: 系统代理状态已更新。
tunModeToggled:
title: TUN 模式
body: TUN 模式状态已更新。
lightweightModeEntered:
title: 轻量模式
body: 已进入轻量模式。
profilesReactivated:
title: 订阅
body: 订阅已激活。
appQuit:
title: 即将退出
body: Clash Verge 即将退出。
appHidden:
title: 应用已隐藏
body: Clash Verge 正在后台运行。
service:
adminInstallPrompt: 安装服务需要管理员权限
adminUninstallPrompt: 写在服务需要管理员权限
tray:
dashboard: 仪表板
ruleMode: 规则模式
globalMode: 全局模式
directMode: 直连模式
outboundModes: 出站模式
rule: 规则
direct: 直连
global: 全局
profiles: 订阅
proxies: 代理
systemProxy: 系统代理
tunMode: TUN 模式
closeAllConnections: 关闭所有连接
lightweightMode: 轻量模式
copyEnv: 复制环境变量
confDir: 配置目录
coreDir: 内核目录
logsDir: 日志目录
openDir: 打开目录
appLog: 应用日志
coreLog: 内核日志
restartClash: 重启 Clash 内核
restartApp: 重启应用
vergeVersion: Verge 版本
more: 更多
exit: 退出
tooltip:
systemProxy: 系统代理
tun: TUN
profile: 订阅
-60
View File
@@ -1,60 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: 儀表板
body: 儀表板顯示狀態已更新。
clashModeChanged:
title: 模式切換
body: 已切換至 {mode}。
systemProxyToggled:
title: 系統代理
body: 系統代理狀態已更新。
tunModeToggled:
title: 虛擬網路介面卡模式
body: 已更新虛擬網路介面卡模式狀態。
lightweightModeEntered:
title: 輕量模式
body: 已進入輕量模式。
profilesReactivated:
title: 訂閱
body: 訂閱已啟用。
appQuit:
title: 即將退出
body: Clash Verge 即將退出。
appHidden:
title: 應用已隱藏
body: Clash Verge 正在背景執行。
service:
adminInstallPrompt: 安裝服務需要管理員權限
adminUninstallPrompt: 卸载服務需要管理員權限
tray:
dashboard: 儀表板
ruleMode: 規則模式
globalMode: 全域模式
directMode: 直連模式
outboundModes: 出站模式
rule: 規則
direct: 直連
global: 全域
profiles: 訂閱
proxies: 代理
systemProxy: 系統代理
tunMode: 虛擬網路介面卡模式
closeAllConnections: 關閉所有連線
lightweightMode: 輕量模式
copyEnv: 複製環境變數
confDir: 設定目錄
coreDir: 核心目錄
logsDir: 日誌目錄
openDir: 開啟目錄
appLog: 應用程式日誌
coreLog: 核心日誌
restartClash: 重新啟動 Clash 核心
restartApp: 重新啟動應用程式
vergeVersion: Verge 版本
more: 更多
exit: 離開
tooltip:
systemProxy: 系統代理
tun: 虛擬網路介面卡
profile: 訂閱
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/bash
chmod +x /usr/bin/clash-verge-service-install
chmod +x /usr/bin/clash-verge-service-uninstall
chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/clash-verge-service
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/bash
/usr/bin/clash-verge-service-uninstall
/usr/bin/uninstall-service
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>io.github.clash-verge-rev.clash-verge-rev.service</string>
</array>
</dict>
</plist>
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
[toolchain]
channel = "1.91.0"
components = ["rustfmt", "clippy"]
+2 -2
View File
@@ -1,4 +1,4 @@
max_width = 120
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
@@ -6,7 +6,7 @@ use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2024"
edition = "2021"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
+111
View File
@@ -0,0 +1,111 @@
use crate::singleton;
use anyhow::Result;
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::OnceCell;
pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
pub struct CacheEntry<T> {
pub value: Arc<T>,
pub expires_at: Instant,
}
pub struct Cache<T> {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
}
impl<T> Cache<T> {
fn new() -> Self {
Cache {
map: DashMap::new(),
}
}
pub fn make_key(prefix: &str, id: &str) -> String {
format!("{prefix}:{id}")
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<T>
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
T: Send + Sync + 'static,
{
loop {
let now = Instant::now();
let key_cloned = key.clone();
// Get or create the cell
let cell = self
.map
.entry(key_cloned.clone())
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
// Check if we have a valid cached entry
if let Some(entry) = cell.get() {
if entry.expires_at > now {
return Arc::clone(&entry.value);
}
// Entry is expired, remove it
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
continue; // Retry with fresh cell
}
// Try to set a new value
let value = fetch_fn().await;
let entry = Box::new(CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
});
match cell.set(entry) {
Ok(_) => {
// Successfully set the value, it must exist now
if let Some(set_entry) = cell.get() {
return Arc::clone(&set_entry.value);
}
}
Err(_) => {
if let Some(existing_entry) = cell.get() {
if existing_entry.expires_at > Instant::now() {
return Arc::clone(&existing_entry.value);
}
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
}
}
}
}
}
// pub fn clean_key(&self, key: &str) {
// self.map.remove(key);
// }
// TODO
pub fn clean_default_keys(&self) {
// logging!(info, Type::Cache, "Cleaning proxies keys");
// let proxies_key = Self::make_key("proxies", "default");
// self.map.remove(&proxies_key);
// logging!(info, Type::Cache, "Cleaning providers keys");
// let providers_key = Self::make_key("providers", "default");
// self.map.remove(&providers_key);
// !The frontend goes crash if we clean the clash_config cache
// logging!(info, Type::Cache, "Cleaning clash config keys");
// let clash_config_key = Self::make_key("clash_config", "default");
// self.map.remove(&clash_config_key);
}
}
pub type CacheService = Cache<Result<String>>;
pub type CacheProxy = Cache<Value>;
singleton!(Cache<Value>, PROXY_INSTANCE);
singleton!(Cache<Result<String>>, SERVICE_INSTANCE);
+85 -92
View File
@@ -1,58 +1,37 @@
use super::CmdResult;
use crate::core::sysopt::Sysopt;
use crate::utils::resolve::ui::{self, UiReadyStage};
use crate::{
cmd::StringifyErr as _,
feat,
utils::dirs::{self, PathBufExec as _},
feat, logging,
utils::{dirs, logging::Type},
wrap_err,
};
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::path::Path;
use tauri::{AppHandle, Manager as _};
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
use tauri::{AppHandle, Manager};
/// 打开应用程序所在目录
#[tauri::command]
pub async fn open_app_dir() -> CmdResult<()> {
let app_dir = dirs::app_home_dir().stringify_err()?;
open::that(app_dir).stringify_err()
let app_dir = wrap_err!(dirs::app_home_dir())?;
wrap_err!(open::that(app_dir))
}
/// 打开核心所在目录
#[tauri::command]
pub async fn open_core_dir() -> CmdResult<()> {
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
open::that(core_dir).stringify_err()
wrap_err!(open::that(core_dir))
}
/// 打开日志目录
#[tauri::command]
pub async fn open_logs_dir() -> CmdResult<()> {
let log_dir = dirs::app_logs_dir().stringify_err()?;
open::that(log_dir).stringify_err()
let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir))
}
/// 打开网页链接
#[tauri::command]
pub fn open_web_url(url: String) -> CmdResult<()> {
open::that(url.as_str()).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Verge 最新日志
#[tauri::command]
pub async fn open_app_log() -> CmdResult<()> {
open::that(dirs::app_latest_log().stringify_err()?).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Clash 最新日志
#[tauri::command]
pub async fn open_core_log() -> CmdResult<()> {
open::that(dirs::clash_latest_log().stringify_err()?).stringify_err()
wrap_err!(open::that(url))
}
/// 打开/关闭开发者工具
@@ -82,40 +61,43 @@ pub async fn restart_app() -> CmdResult<()> {
/// 获取便携版标识
#[tauri::command]
pub fn get_portable_flag() -> bool {
*dirs::PORTABLE_FLAG.get().unwrap_or(&false)
pub fn get_portable_flag() -> CmdResult<bool> {
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
}
/// 获取应用目录
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
let app_home_dir = wrap_err!(dirs::app_home_dir())?
.to_string_lossy()
.to_string();
Ok(app_home_dir)
}
/// 获取当前自启动状态
#[tauri::command]
pub fn get_auto_launch_status() -> CmdResult<bool> {
Sysopt::global().get_launch_status().stringify_err()
use crate::core::sysopt::Sysopt;
wrap_err!(Sysopt::global().get_launch_status())
}
/// 下载图标缓存
#[tauri::command]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let icon_cache_dir = dirs::app_home_dir().stringify_err()?.join("icons").join("cache");
let icon_path = icon_cache_dir.join(name.as_str());
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
let icon_path = icon_cache_dir.join(&name);
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().into());
return Ok(icon_path.to_string_lossy().to_string());
}
if !icon_cache_dir.exists() {
let _ = fs::create_dir_all(&icon_cache_dir).await;
let _ = std::fs::create_dir_all(&icon_cache_dir);
}
let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str()));
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
let response = reqwest::get(url.as_str()).await.stringify_err()?;
let response = wrap_err!(reqwest::get(&url).await)?;
let content_type = response
.headers()
@@ -125,44 +107,46 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
let is_image = content_type.starts_with("image/");
let content = response.bytes().await.stringify_err()?;
let content = wrap_err!(response.bytes().await)?;
let is_html = content.len() > 15
&& (content.starts_with(b"<!DOCTYPE html") || content.starts_with(b"<html") || content.starts_with(b"<?xml"));
&& (content.starts_with(b"<!DOCTYPE html")
|| content.starts_with(b"<html")
|| content.starts_with(b"<?xml"));
if is_image && !is_html {
{
let mut file = match fs::File::create(&temp_path).await {
let mut file = match std::fs::File::create(&temp_path) {
Ok(file) => file,
Err(_) => {
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().into());
return Ok(icon_path.to_string_lossy().to_string());
}
return Err("Failed to create temporary file".into());
}
};
file.write_all(content.as_ref()).await.stringify_err()?;
file.flush().await.stringify_err()?;
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
}
if !icon_path.exists() {
match fs::rename(&temp_path, &icon_path).await {
match std::fs::rename(&temp_path, &icon_path) {
Ok(_) => {}
Err(_) => {
let _ = temp_path.remove_if_exists().await;
let _ = std::fs::remove_file(&temp_path);
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().into());
return Ok(icon_path.to_string_lossy().to_string());
}
}
}
} else {
let _ = temp_path.remove_if_exists().await;
let _ = std::fs::remove_file(&temp_path);
}
Ok(icon_path.to_string_lossy().into())
Ok(icon_path.to_string_lossy().to_string())
} else {
let _ = temp_path.remove_if_exists().await;
Err(format!("下载的内容不是有效图片: {}", url.as_str()).into())
let _ = std::fs::remove_file(&temp_path);
Err(format!("下载的内容不是有效图片: {url}"))
}
}
@@ -175,70 +159,79 @@ pub struct IconInfo {
/// 复制图标文件
#[tauri::command]
pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
let file_path = Path::new(path.as_str());
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
use std::{fs, path::Path};
let icon_dir = dirs::app_home_dir().stringify_err()?.join("icons");
let file_path = Path::new(&path);
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
if !icon_dir.exists() {
let _ = fs::create_dir_all(&icon_dir).await;
let _ = fs::create_dir_all(&icon_dir);
}
let ext: String = match file_path.extension() {
Some(e) => e.to_string_lossy().into(),
None => "ico".into(),
let ext = match file_path.extension() {
Some(e) => e.to_string_lossy().to_string(),
None => "ico".to_string(),
};
let dest_path = icon_dir.join(format!(
"{0}-{1}.{ext}",
icon_info.name.as_str(),
icon_info.current_t.as_str()
icon_info.name, icon_info.current_t
));
if file_path.exists() {
if icon_info.previous_t.trim() != "" {
icon_dir
.join(format!(
"{0}-{1}.png",
icon_info.name.as_str(),
icon_info.previous_t.as_str()
))
.remove_if_exists()
.await
.unwrap_or_default();
icon_dir
.join(format!(
"{0}-{1}.ico",
icon_info.name.as_str(),
icon_info.previous_t.as_str()
))
.remove_if_exists()
.await
.unwrap_or_default();
fs::remove_file(
icon_dir.join(format!("{0}-{1}.png", icon_info.name, icon_info.previous_t)),
)
.unwrap_or_default();
fs::remove_file(
icon_dir.join(format!("{0}-{1}.ico", icon_info.name, icon_info.previous_t)),
)
.unwrap_or_default();
}
logging!(
info,
Type::Cmd,
true,
"Copying icon file path: {:?} -> file dist: {:?}",
path,
dest_path
);
match fs::copy(file_path, &dest_path).await {
Ok(_) => Ok(dest_path.to_string_lossy().into()),
Err(err) => Err(err.to_string().into()),
match fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
}
} else {
Err("file not found".into())
Err("file not found".to_string())
}
}
/// 通知UI已准备就绪
#[tauri::command]
pub fn notify_ui_ready() {
logging!(info, Type::Cmd, "前端UI已准备就绪");
ui::mark_ui_ready();
pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "前端UI已准备就绪");
crate::utils::resolve::ui::mark_ui_ready();
Ok(())
}
/// UI加载阶段
#[tauri::command]
pub fn update_ui_stage(stage: UiReadyStage) {
logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage);
ui::update_ui_ready_stage(stage);
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI加载阶段更新: {stage}");
use crate::utils::resolve::ui::UiReadyStage;
let stage_enum = match stage.as_str() {
"NotStarted" => UiReadyStage::NotStarted,
"Loading" => UiReadyStage::Loading,
"DomReady" => UiReadyStage::DomReady,
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready,
_ => {
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
return Err(format!("未知的UI加载阶段: {stage}"));
}
};
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(())
}
-34
View File
@@ -1,34 +0,0 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, feat};
use feat::LocalBackupFile;
use smartstring::alias::String;
/// Create a local backup
#[tauri::command]
pub async fn create_local_backup() -> CmdResult<()> {
feat::create_local_backup().await.stringify_err()
}
/// List local backups
#[tauri::command]
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().await.stringify_err()
}
/// Delete local backup
#[tauri::command]
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
feat::delete_local_backup(filename).await.stringify_err()
}
/// Restore local backup
#[tauri::command]
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
feat::restore_local_backup(filename).await.stringify_err()
}
/// Export local backup to a user selected destination
#[tauri::command]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination).await.stringify_err()
}
+436 -64
View File
@@ -1,17 +1,21 @@
use super::CmdResult;
use crate::feat;
use crate::utils::dirs;
use crate::{
cmd::StringifyErr as _,
config::{ClashInfo, Config},
constants,
core::{CoreManager, handle, validate::CoreConfigValidator},
cache::CacheProxy,
config::Config,
core::{CoreManager, handle},
};
use crate::{
config::*,
feat,
ipc::{self, IpcManager},
logging,
utils::logging::Type,
wrap_err,
};
use clash_verge_logging::{Type, logging, logging_error};
use compact_str::CompactString;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
use std::time::Duration;
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
/// 复制Clash环境变量
#[tauri::command]
@@ -23,13 +27,13 @@ pub async fn copy_clash_env() -> CmdResult {
/// 获取Clash信息
#[tauri::command]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.data_arc().get_client_info())
Ok(Config::clash().await.latest_ref().get_client_info())
}
/// 修改Clash配置
#[tauri::command]
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
feat::patch_clash(&payload).await.stringify_err()
wrap_err!(feat::patch_clash(payload).await)
}
/// 修改Clash模式
@@ -44,30 +48,35 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
match CoreManager::global()
.change_core(Some(clash_core.clone()))
.await
{
Ok(_) => {
logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await);
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
Ok(_) => {
logging!(info, Type::Core, "core changed and restarted to {clash_core}");
handle::Handle::notice_message("config_core::change_success", clash_core);
logging!(
info,
Type::Core,
"core changed and restarted to {clash_core}"
);
handle::Handle::notice_message("config_core::change_success", &clash_core);
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let error_msg: String = format!("Core changed but failed to restart: {err}").into();
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
let error_msg = format!("Core changed but failed to restart: {err}");
logging!(error, Type::Core, "{error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
}
}
Err(err) => {
let error_msg: String = err;
let error_msg = err.to_string();
logging!(error, Type::Core, "failed to change core: {error_msg}");
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
}
@@ -76,7 +85,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
/// 启动核心
#[tauri::command]
pub async fn start_core() -> CmdResult {
let result = CoreManager::global().start_core().await.stringify_err();
let result = wrap_err!(CoreManager::global().start_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
@@ -86,8 +95,7 @@ pub async fn start_core() -> CmdResult {
/// 关闭核心
#[tauri::command]
pub async fn stop_core() -> CmdResult {
logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await);
let result = CoreManager::global().stop_core().await.stringify_err();
let result = wrap_err!(CoreManager::global().stop_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
@@ -97,21 +105,34 @@ pub async fn stop_core() -> CmdResult {
/// 重启核心
#[tauri::command]
pub async fn restart_core() -> CmdResult {
logging_error!(Type::Core, Config::profiles().await.latest_arc().save_file().await);
let result = CoreManager::global().restart_core().await.stringify_err();
let result = wrap_err!(CoreManager::global().restart_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 获取代理延迟
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<serde_json::Value> {
wrap_err!(
IpcManager::global()
.test_proxy_delay(&name, url, timeout)
.await
)
}
/// 测试URL延迟
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
logging!(error, Type::Cmd, "{}", e);
log::error!(target: "app", "{}", e);
10000u32
}
};
@@ -126,11 +147,15 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use tokio::fs;
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
// 保存DNS配置到文件
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
fs::write(&dns_path, yaml_str).await.stringify_err()?;
let yaml_str = serde_yaml_ng::to_string(&dns_config).map_err(|e| e.to_string())?;
fs::write(&dns_path, yaml_str)
.await
.map_err(|e| e.to_string())?;
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
Ok(())
@@ -139,23 +164,34 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
/// 应用或撤销DNS配置
#[tauri::command]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{CoreManager, handle},
utils::dirs,
};
if apply {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found");
return Err("DNS config file not found".into());
}
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
let dns_yaml = tokio::fs::read_to_string(&dns_path).await.map_err(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
e.to_string()
})?;
// 解析DNS配置
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
})?;
let patch_config =
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).map_err(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
e.to_string()
})?;
logging!(info, Type::Config, "Applying DNS config from file");
@@ -164,41 +200,56 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置
Config::runtime().await.edit_draft(|d| {
d.patch_config(&patch);
});
Config::runtime().await.draft_mut().patch_config(patch);
// 重新生成配置
Config::generate().await.stringify_err_log(|err| {
let err = format!("Failed to regenerate config with DNS: {err}");
logging!(error, Type::Config, "{err}");
Config::generate().await.map_err(|err| {
logging!(
error,
Type::Config,
"Failed to regenerate config with DNS: {err}"
);
"Failed to regenerate config with DNS".to_string()
})?;
// 应用新配置
CoreManager::global().update_config().await.stringify_err_log(|err| {
let err = format!("Failed to apply config with DNS: {err}");
logging!(error, Type::Config, "{err}");
CoreManager::global().update_config().await.map_err(|err| {
logging!(
error,
Type::Config,
"Failed to apply config with DNS: {err}"
);
"Failed to apply config with DNS".to_string()
})?;
logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
} else {
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
logging!(info, Type::Config, "DNS settings disabled, regenerating config");
logging!(
info,
Type::Config,
"DNS settings disabled, regenerating config"
);
Config::generate().await.stringify_err_log(|err| {
let err = format!("Failed to regenerate config: {err}");
logging!(error, Type::Config, "{err}");
Config::generate().await.map_err(|err| {
logging!(error, Type::Config, "Failed to regenerate config: {err}");
"Failed to regenerate config".to_string()
})?;
CoreManager::global().update_config().await.stringify_err_log(|err| {
let err = format!("Failed to apply regenerated config: {err}");
logging!(error, Type::Config, "{err}");
CoreManager::global().update_config().await.map_err(|err| {
logging!(
error,
Type::Config,
"Failed to apply regenerated config: {err}"
);
"Failed to apply regenerated config".to_string()
})?;
logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
}
handle::Handle::refresh_clash();
Ok(())
}
@@ -207,7 +258,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
pub fn check_dns_config_exists() -> CmdResult<bool> {
use crate::utils::dirs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
Ok(dns_path.exists())
}
@@ -218,34 +271,353 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
if !fs::try_exists(&dns_path).await.stringify_err()? {
if !fs::try_exists(&dns_path).await.map_err(|e| e.to_string())? {
return Err("DNS config file not found".into());
}
let content = fs::read_to_string(&dns_path).await.stringify_err()?.into();
let content = fs::read_to_string(&dns_path)
.await
.map_err(|e| e.to_string())?;
Ok(content)
}
/// 验证DNS配置文件
#[tauri::command]
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
let app_dir = dirs::app_home_dir().stringify_err()?;
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
use crate::{core::CoreManager, utils::dirs};
let app_dir = dirs::app_home_dir().map_err(|e| e.to_string())?;
let dns_path = app_dir.join("dns_config.yaml");
let dns_path_str = dns_path.to_str().unwrap_or_default();
if !dns_path.exists() {
return Ok((false, "DNS config file not found".into()));
return Ok((false, "DNS config file not found".to_string()));
}
CoreConfigValidator::validate_config_file(dns_path_str, None)
match CoreManager::global()
.validate_config_file(dns_path_str, None)
.await
.stringify_err()
{
Ok(result) => Ok(result),
Err(e) => Err(e.to_string()),
}
}
/// 获取Clash版本信息
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<Vec<CompactString>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs)
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_version().await)
}
/// 获取Clash配置
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
cache.map.remove(&key);
get_clash_config().await
}
/// 更新地理数据
#[tauri::command]
pub async fn update_geo_data() -> CmdResult {
wrap_err!(IpcManager::global().update_geo_data().await)
}
/// 升级Clash核心
#[tauri::command]
pub async fn upgrade_clash_core() -> CmdResult {
wrap_err!(IpcManager::global().upgrade_core().await)
}
/// 获取规则
#[tauri::command]
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rules().await)
}
/// 更新代理选择
#[tauri::command]
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
}
/// 获取代理提供者
#[tauri::command]
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_providers_proxies().await)
}
/// 获取规则提供者
#[tauri::command]
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rule_providers().await)
}
/// 代理提供者健康检查
#[tauri::command]
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
wrap_err!(
IpcManager::global()
.proxy_provider_health_check(&name)
.await
)
}
/// 更新代理提供者
#[tauri::command]
pub async fn update_proxy_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
}
/// 更新规则提供者
#[tauri::command]
pub async fn update_rule_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
}
/// 获取连接
#[tauri::command]
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_connections().await)
}
/// 删除连接
#[tauri::command]
pub async fn delete_clash_connection(id: String) -> CmdResult {
wrap_err!(IpcManager::global().delete_connection(&id).await)
}
/// 关闭所有连接
#[tauri::command]
pub async fn close_all_clash_connections() -> CmdResult {
wrap_err!(IpcManager::global().close_all_connections().await)
}
/// 获取流量数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
let traffic = crate::ipc::get_current_traffic().await;
let result = serde_json::json!({
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate,
"last_updated": traffic.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 获取内存数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
let memory = crate::ipc::get_current_memory().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
};
let result = serde_json::json!({
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": usage_percent,
"last_updated": memory.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
#[tauri::command]
pub async fn start_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
// 新的IPC监控在首次访问时自动启动
// 触发一次访问以确保监控器已初始化
let _ = crate::ipc::get_current_traffic().await;
let _ = crate::ipc::get_current_memory().await;
logging!(info, Type::Ipc, "IPC流式监控已激活");
Ok(())
}
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
#[tauri::command]
pub async fn stop_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
// 新的IPC监控是持久的,无需显式停止
logging!(info, Type::Ipc, "IPC流式监控继续运行");
Ok(())
}
/// 获取格式化的流量数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
logging!(trace, Type::Ipc, "获取格式化流量数据");
let (up_rate, down_rate, total_up, total_down, is_fresh) =
crate::ipc::get_formatted_traffic().await;
let result = serde_json::json!({
"up_rate_formatted": up_rate,
"down_rate_formatted": down_rate,
"total_up_formatted": total_up,
"total_down_formatted": total_down,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
);
Ok(result)
}
/// 获取格式化的内存数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
logging!(info, Type::Ipc, "获取格式化内存数据");
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
let result = serde_json::json!({
"inuse_formatted": inuse,
"oslimit_formatted": oslimit,
"usage_percent": usage_percent,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
);
Ok(result)
}
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
#[tauri::command]
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
logging!(debug, Type::Ipc, "获取系统监控概览");
// 并发获取流量和内存数据
let (traffic, memory) = tokio::join!(
crate::ipc::get_current_traffic(),
crate::ipc::get_current_memory()
);
let (traffic_formatted, memory_formatted) = tokio::join!(
crate::ipc::get_formatted_traffic(),
crate::ipc::get_formatted_memory()
);
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
let result = serde_json::json!({
"traffic": {
"raw": {
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate
},
"formatted": {
"up_rate": traffic_formatted.0,
"down_rate": traffic_formatted.1,
"total_up": traffic_formatted.2,
"total_down": traffic_formatted.3
},
"is_fresh": traffic_is_fresh
},
"memory": {
"raw": {
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
}
},
"formatted": {
"inuse": memory_formatted.0,
"oslimit": memory_formatted.1,
"usage_percent": memory_formatted.2
},
"is_fresh": memory_is_fresh
},
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
});
Ok(result)
}
/// 获取代理组延迟
#[tauri::command]
pub async fn get_group_proxy_delays(
group_name: String,
url: Option<String>,
timeout: Option<i32>,
) -> CmdResult<serde_json::Value> {
wrap_err!(
IpcManager::global()
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
.await
)
}
/// 检查调试是否启用
#[tauri::command]
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
match IpcManager::global().is_debug_enabled().await {
Ok(enabled) => Ok(enabled),
Err(_) => Ok(false),
}
}
/// 垃圾回收
#[tauri::command]
pub async fn clash_gc() -> CmdResult {
wrap_err!(IpcManager::global().gc().await)
}
/// 获取日志 (使用新的流式实现)
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
Ok(ipc::get_logs_json().await)
}
/// 启动日志监控
#[tauri::command]
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
ipc::start_logs_monitoring(level).await;
Ok(())
}
/// 停止日志监控
#[tauri::command]
pub async fn stop_logs_monitoring() -> CmdResult {
ipc::stop_logs_monitoring().await;
Ok(())
}
/// 清除日志
#[tauri::command]
pub async fn clear_logs() -> CmdResult {
ipc::clear_logs().await;
Ok(())
}
File diff suppressed because it is too large Load Diff
@@ -1,119 +0,0 @@
use std::sync::Arc;
use clash_verge_logging::{Type, logging};
use regex::Regex;
use reqwest::{Client, cookie::Jar};
use super::UnlockItem;
use super::utils::{country_code_to_emoji, get_local_date_string};
pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
let cookie_store = Arc::new(Jar::default());
let client_with_cookies = match Client::builder()
.use_rustls_tls()
.user_agent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
)
.cookie_provider(Arc::clone(&cookie_store))
.build()
{
Ok(client) => client,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to create client with cookies for Bahamut Anime: {}",
e
);
client.clone()
}
};
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
let device_id = match client_with_cookies.get(device_url).send().await {
Ok(response) => match response.text().await {
Ok(text) => match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) {
Ok(re) => re
.captures(&text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.unwrap_or_default(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile deviceid regex for Bahamut Anime: {}",
e
);
String::new()
}
},
Err(_) => String::new(),
},
Err(_) => String::new(),
};
if device_id.is_empty() {
return UnlockItem {
name: "Bahamut Anime".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
let url = format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
let token_result = match client_with_cookies.get(&url).send().await {
Ok(response) => match response.text().await {
Ok(body) => {
if body.contains("animeSn") {
Some(body)
} else {
None
}
}
Err(_) => None,
},
Err(_) => None,
};
if token_result.is_none() {
return UnlockItem {
name: "Bahamut Anime".to_string(),
status: "No".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
let region = match client_with_cookies.get("https://ani.gamer.com.tw/").send().await {
Ok(response) => match response.text().await {
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
Ok(region_re) => region_re.captures(&body).and_then(|caps| caps.get(1)).map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{emoji}{country_code}")
}),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile region regex for Bahamut Anime: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
};
UnlockItem {
name: "Bahamut Anime".to_string(),
status: "Yes".to_string(),
region,
check_time: Some(get_local_date_string()),
}
}
@@ -1,91 +0,0 @@
use reqwest::Client;
use serde_json::Value;
use super::UnlockItem;
use super::utils::get_local_date_string;
pub(super) async fn check_bilibili_china_mainland(client: &Client) -> UnlockItem {
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=82846771&qn=0&type=&otype=json&ep_id=307247&fourk=1&fnver=0&fnval=16&module=bangumi";
match client.get(url).send().await {
Ok(response) => match response.json::<Value>().await {
Ok(body) => {
let status = body
.get("code")
.and_then(|v| v.as_i64())
.map(|code| {
if code == 0 {
"Yes"
} else if code == -10403 {
"No"
} else {
"Failed"
}
})
.unwrap_or("Failed");
UnlockItem {
name: "哔哩哔哩大陆".to_string(),
status: status.to_string(),
region: None,
check_time: Some(get_local_date_string()),
}
}
Err(_) => UnlockItem {
name: "哔哩哔哩大陆".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
},
Err(_) => UnlockItem {
name: "哔哩哔哩大陆".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
}
}
pub(super) async fn check_bilibili_hk_mc_tw(client: &Client) -> UnlockItem {
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=18281381&cid=29892777&qn=0&type=&otype=json&ep_id=183799&fourk=1&fnver=0&fnval=16&module=bangumi";
match client.get(url).send().await {
Ok(response) => match response.json::<Value>().await {
Ok(body) => {
let status = body
.get("code")
.and_then(|v| v.as_i64())
.map(|code| {
if code == 0 {
"Yes"
} else if code == -10403 {
"No"
} else {
"Failed"
}
})
.unwrap_or("Failed");
UnlockItem {
name: "哔哩哔哩港澳台".to_string(),
status: status.to_string(),
region: None,
check_time: Some(get_local_date_string()),
}
}
Err(_) => UnlockItem {
name: "哔哩哔哩港澳台".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
},
Err(_) => UnlockItem {
name: "哔哩哔哩港澳台".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
}
}

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