mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Fri Nov 14 19:38:20 CET 2025
This commit is contained in:
@@ -1181,3 +1181,4 @@ Update On Mon Nov 10 19:41:15 CET 2025
|
||||
Update On Tue Nov 11 19:39:14 CET 2025
|
||||
Update On Wed Nov 12 19:37:25 CET 2025
|
||||
Update On Thu Nov 13 19:41:20 CET 2025
|
||||
Update On Fri Nov 14 19:38:12 CET 2025
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 1,
|
||||
"latest": {
|
||||
"mihomo": "v1.19.16",
|
||||
"mihomo_alpha": "alpha-0b3159b",
|
||||
"mihomo_alpha": "alpha-f6e494e",
|
||||
"clash_rs": "v0.9.2",
|
||||
"clash_premium": "2023-09-05-gdcc8d87",
|
||||
"clash_rs_alpha": "0.9.2-alpha+sha.87c7b2c"
|
||||
@@ -69,5 +69,5 @@
|
||||
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
|
||||
}
|
||||
},
|
||||
"updated_at": "2025-11-11T22:21:17.993Z"
|
||||
"updated_at": "2025-11-13T22:21:28.265Z"
|
||||
}
|
||||
|
||||
@@ -3,3 +3,7 @@ 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
@@ -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. 请 **务必** 查看 [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) 版本,确定问题是否仍然存在
|
||||
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) 版本,确定问题是否仍然存在
|
||||
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 [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
|
||||
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
|
||||
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: 软件版本 / 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)
|
||||
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)
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -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. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
|
||||
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
|
||||
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 [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
|
||||
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
|
||||
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||
|
||||
- type: textarea
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ body:
|
||||
- type: input
|
||||
id: verge-version
|
||||
attributes:
|
||||
label: 软件版本 / Verge Version
|
||||
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
|
||||
label: 软件版本 / CVR Version
|
||||
description: 请提供你使用的 CVR 具体版本 / Please provide the specific version of CVR you are using
|
||||
validations:
|
||||
required: true
|
||||
|
||||
+4
-4
@@ -185,18 +185,18 @@ jobs:
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -f "Changelog.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.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"
|
||||
echo "No update sections found in Changelog.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
echo "Changelog.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
|
||||
+69
-41
@@ -35,20 +35,7 @@ jobs:
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
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
|
||||
run: bash ./scripts/extract_update_logs.sh
|
||||
shell: bash
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -90,20 +77,20 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
@@ -158,7 +145,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.91.0"
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
@@ -169,7 +159,8 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -197,6 +188,14 @@ jobs:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -205,6 +204,13 @@ jobs:
|
||||
- name: Release ${{ env.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:
|
||||
@@ -248,7 +254,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.91.0"
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
@@ -259,7 +268,8 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -272,6 +282,14 @@ jobs:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -329,6 +347,13 @@ 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
|
||||
@@ -391,7 +416,8 @@ jobs:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -404,6 +430,14 @@ jobs:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
@@ -419,6 +453,13 @@ jobs:
|
||||
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
|
||||
@@ -482,20 +523,7 @@ jobs:
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
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
|
||||
run: bash ./scripts/extract_update_logs.sh
|
||||
shell: bash
|
||||
|
||||
- name: Install Node
|
||||
@@ -538,20 +566,20 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
+9
-5
@@ -74,11 +74,7 @@ jobs:
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add Rust Target
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
uses: dtolnay/rust-toolchain@1.91.0
|
||||
|
||||
- name: Rust Cache
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
@@ -118,6 +114,14 @@ 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
|
||||
|
||||
+20
-29
@@ -22,6 +22,7 @@ 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:
|
||||
@@ -30,13 +31,18 @@ jobs:
|
||||
- 'src-tauri/**'
|
||||
|
||||
- name: Skip if src-tauri not changed
|
||||
if: steps.check_changes.outputs.rust != 'true'
|
||||
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: steps.check_changes.outputs.rust == 'true'
|
||||
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@v4
|
||||
|
||||
@@ -53,9 +59,10 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
cache-all-crates: false
|
||||
shared-key: autobuild-shared
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -63,29 +70,13 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
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
|
||||
working-directory: ./src-tauri
|
||||
run: cargo clippy-all
|
||||
|
||||
- name: Run Logging Check
|
||||
working-directory: ./src-tauri
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
|
||||
clash-verge-logging-check
|
||||
|
||||
+15
-32
@@ -5,6 +5,7 @@ on:
|
||||
# ! 不再使用 workflow_dispatch 触发。
|
||||
# workflow_dispatch:
|
||||
push:
|
||||
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
permissions: write-all
|
||||
@@ -72,20 +73,7 @@ jobs:
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
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
|
||||
run: bash ./scripts/extract_update_logs.sh
|
||||
shell: bash
|
||||
|
||||
- name: Set Env
|
||||
@@ -143,7 +131,7 @@ jobs:
|
||||
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
|
||||
body_path: release.txt
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# generate_release_notes: true
|
||||
|
||||
@@ -213,7 +201,8 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.5.23
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -230,7 +219,7 @@ jobs:
|
||||
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
|
||||
releaseBody: "Draft release, will be updated later."
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
@@ -354,6 +343,7 @@ 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: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
@@ -409,7 +399,8 @@ jobs:
|
||||
|
||||
- name: Tauri build
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.5.23
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -446,6 +437,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: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
- name: Portable Bundle
|
||||
@@ -454,6 +446,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-update:
|
||||
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||
name: Release Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [update_tag]
|
||||
@@ -480,6 +473,7 @@ 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:
|
||||
@@ -505,6 +499,7 @@ 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]
|
||||
@@ -528,6 +523,7 @@ jobs:
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
notify-telegram:
|
||||
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||
name: Notify Telegram
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
@@ -543,20 +539,7 @@ jobs:
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
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
|
||||
run: bash ./scripts/extract_update_logs.sh
|
||||
shell: bash
|
||||
|
||||
- name: Install Node
|
||||
|
||||
@@ -11,3 +11,4 @@ scripts/_env.sh
|
||||
.idea
|
||||
.old
|
||||
.eslintcache
|
||||
target
|
||||
@@ -1,24 +1,57 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[pre-commit] Running lint-staged for JS/TS files..."
|
||||
# Auto-fix staged JS/TS files, print warnings but don't fail commit
|
||||
npx lint-staged || true
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# Check staged Rust files
|
||||
RUST_FILES=$(git diff --cached --name-only | grep -E '^src-tauri/.*\.rs$' || true)
|
||||
if [ -n "$RUST_FILES" ]; then
|
||||
echo "[pre-commit] Running rustfmt and clippy on staged Rust files..."
|
||||
cd src-tauri || exit
|
||||
|
||||
# Auto-format Rust code
|
||||
cargo fmt
|
||||
|
||||
# Lint with clippy, print warnings but don't fail commit
|
||||
cargo clippy || echo "⚠️ clippy found issues, but commit will continue."
|
||||
|
||||
cd ..
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "❌ pnpm is required for pre-commit checks."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[pre-commit] Checks completed. Some warnings may exist, please review."
|
||||
exit 0
|
||||
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..."
|
||||
(
|
||||
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
|
||||
fi
|
||||
clash-verge-logging-check
|
||||
)
|
||||
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
|
||||
|
||||
echo "[pre-commit] All checks completed successfully."
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
remote_name="$1"
|
||||
remote_name="${1:-origin}"
|
||||
remote_url="${2:-unknown}"
|
||||
|
||||
# --- Rust clippy for staged files in src-tauri ---
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
echo "[pre-push] Running clippy on src-tauri..."
|
||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml -- -D warnings || {
|
||||
echo "❌ Clippy found issues in src-tauri. Please fix them before pushing."
|
||||
exit 1
|
||||
}
|
||||
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
|
||||
fi
|
||||
|
||||
# --- JS/TS format check only for main repo ---
|
||||
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..."
|
||||
if ! pnpm format:check; 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
|
||||
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
|
||||
)
|
||||
else
|
||||
echo "[pre-push] Remote '$remote_name' does not exist. Skipping format check."
|
||||
echo "[pre-push] ⚠️ cargo not found; skipping Rust checks."
|
||||
fi
|
||||
|
||||
echo "[pre-push] All checks passed."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# README.md
|
||||
# UPDATELOG.md
|
||||
# Changelog.md
|
||||
# CONTRIBUTING.md
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs.
|
||||
|
||||
## Development Setup
|
||||
|
||||
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
## v2.4.4
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- Linux 无法切换 TUN 堆栈
|
||||
- macOS service 启动项显示名称(试验性修改)
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.16**
|
||||
- 支持连接页面各个项目的排序
|
||||
- 实现可选的自动备份
|
||||
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 替换前端信息编辑组件,提供更好性能
|
||||
- 优化后端内存和性能表现
|
||||
- 防止退出时可能的禁用 TUN 失败
|
||||
- i18n 支持
|
||||
- 优化备份设置布局
|
||||
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
|
||||
|
||||
</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 异常标题栏按钮
|
||||
- 修复架构升级后节点测速功能异常
|
||||
- 修复架构升级后流量统计功能异常
|
||||
- 修复架构升级后日志功能异常
|
||||
- 修复外部控制器跨域配置保存问题
|
||||
- 修复首页端口显示不一致问题
|
||||
- 修复首页流量统计刻度线显示问题
|
||||
- 修复日志页面按钮功能混淆问题
|
||||
- 修复日志等级设置保存问题
|
||||
- 修复日志等级异常过滤
|
||||
- 修复清理日志天数功能异常
|
||||
- 修复偶发性启动卡死问题
|
||||
- 修复首页虚拟网卡开关在管理模式下的状态问题
|
||||
|
||||
### 🔧 技术改进
|
||||
|
||||
- 统一使用新的内核通信方式
|
||||
- 新增外部控制器配置界面
|
||||
- 改进跨平台兼容性支持
|
||||
@@ -9,6 +9,16 @@
|
||||
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 |
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
files:
|
||||
- source: /src/locales/en.json
|
||||
translation: /src/locales
|
||||
multilingual: 1
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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.
|
||||
@@ -1,177 +1,3 @@
|
||||
## v2.4.3
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.14**
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
- 新增链式代理图形化设置功能
|
||||
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
|
||||
- 监听关机事件,自动关闭系统代理
|
||||
- 主界面“当前节点”卡片新增“延迟测试”按钮
|
||||
- 新增批量选择配置文件功能
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||
- 重构并简化窗口创建流程
|
||||
- 重构日志系统,单个日志默认最大 10 MB
|
||||
- 优化前端资源占用
|
||||
- 改进 macos 下系统代理设置的方法
|
||||
- 优化 TUN 模式可用性的判断
|
||||
- 移除流媒体检测的系统级提示(使用软件内通知)
|
||||
- 优化后端 i18n 资源占用
|
||||
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
|
||||
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
|
||||
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
|
||||
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
|
||||
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- macOS Tun/系统代理 模式下图标大小不统一
|
||||
- 托盘节点切换不再显示隐藏组
|
||||
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
|
||||
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
|
||||
- 修复 Webdav 恢复备份不重启
|
||||
- 修复 Linux 开机后无法正常代理需要手动设置
|
||||
- 修复增加订阅或导入订阅文件时订阅页面无更新
|
||||
- 修复系统代理守卫功能不工作
|
||||
- 修复 KDE + Wayland 下多屏显示 UI 异常
|
||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复 Linux GNOME/KDE 桌面下,应用主题颜色选择“系统”后,不随操作系统主题(Dark/Light)切换
|
||||
|
||||
## 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
|
||||
|
||||
### 🐞 修复问题
|
||||
@@ -0,0 +1,125 @@
|
||||
<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 |
|
||||
| ----------------------------------- | ------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,125 @@
|
||||
<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 |
|
||||
| ----------------------------------- | ----------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,125 @@
|
||||
<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>
|
||||
|
||||
## プレビュー
|
||||
|
||||
| ダーク | ライト |
|
||||
| --------------------------------------- | ---------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## インストール
|
||||
|
||||
[リリースページ](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) を参照してください。
|
||||
@@ -0,0 +1,125 @@
|
||||
<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>
|
||||
|
||||
## 미리보기
|
||||
|
||||
| 다크 | 라이트 |
|
||||
| ------------------------------------ | --------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## 설치
|
||||
|
||||
[릴리스 페이지](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)를 참고하세요.
|
||||
@@ -0,0 +1,121 @@
|
||||
<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>
|
||||
## Предпросмотр
|
||||
|
||||
| Тёмная тема | Светлая тема |
|
||||
| ---------------------------------- | ------------------------------------ |
|
||||
|  |  |
|
||||
|
||||
## Установка
|
||||
|
||||
Пожалуйста, перейдите на страницу релизов, чтобы скачать соответствующий установочный пакет: [Страница релизов](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).
|
||||
@@ -1,5 +1,6 @@
|
||||
import eslintReact from "@eslint-react/eslint-plugin";
|
||||
import eslintJS from "@eslint/js";
|
||||
import eslintReact from "@eslint-react/eslint-plugin";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import configPrettier from "eslint-config-prettier";
|
||||
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
||||
import pluginImportX from "eslint-plugin-import-x";
|
||||
@@ -7,7 +8,6 @@ import pluginPrettier from "eslint-plugin-prettier";
|
||||
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";
|
||||
|
||||
@@ -17,6 +17,7 @@ export default defineConfig([
|
||||
|
||||
plugins: {
|
||||
js: eslintJS,
|
||||
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
||||
"react-hooks": pluginReactHooks,
|
||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||
"import-x": pluginImportX,
|
||||
@@ -94,9 +95,10 @@ export default defineConfig([
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
varsIgnorePattern: "^_+$",
|
||||
varsIgnorePattern: "^_",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_+$",
|
||||
argsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^ignore",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -131,4 +133,14 @@ export default defineConfig([
|
||||
"prettier/prettier": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["scripts/**/*.{js,mjs,cjs}", "scripts-workflow/**/*.{js,mjs,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.4",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
@@ -26,11 +26,14 @@
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
|
||||
"lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src",
|
||||
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
|
||||
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
|
||||
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -39,25 +42,26 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^8.14.0",
|
||||
"@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.5.2",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.3.1",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.18.0",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-http": "~2.5.4",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "2.3.3",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.5",
|
||||
"axios": "^1.12.2",
|
||||
"dayjs": "1.11.18",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"i18next": "^25.6.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.54.0",
|
||||
@@ -66,61 +70,61 @@
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-i18next": "16.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "16.3.3",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.59.0",
|
||||
"react-router-dom": "7.9.4",
|
||||
"react-router": "^7.9.6",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"swr": "^2.3.6",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.8",
|
||||
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo"
|
||||
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main",
|
||||
"types-pac": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.0.6",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@tauri-apps/cli": "2.8.4",
|
||||
"@eslint-react/eslint-plugin": "^2.3.5",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tauri-apps/cli": "2.9.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "19.2.4",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cli-color": "^2.0.4",
|
||||
"commander": "^14.0.1",
|
||||
"commander": "^14.0.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.23",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"lint-staged": "^16.2.4",
|
||||
"meta-json-schema": "^1.19.14",
|
||||
"lint-staged": "^16.2.6",
|
||||
"meta-json-schema": "^1.19.16",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"tar": "^7.5.1",
|
||||
"terser": "^5.44.0",
|
||||
"sass": "^1.94.0",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
"eslint --fix",
|
||||
"eslint --fix --max-warnings=0",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
|
||||
Generated
+1056
-822
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,6 @@
|
||||
"groupName": "github actions"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
|
||||
"ignoreDeps": ["criterion"]
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
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
@@ -0,0 +1,43 @@
|
||||
#!/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,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版本重命名版本号
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/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;
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import fs from "fs";
|
||||
import fsp from "fs/promises";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
import path from "path";
|
||||
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import AdmZip from "adm-zip";
|
||||
|
||||
const target = process.argv.slice(2)[0];
|
||||
const alpha = process.argv.slice(2)[1];
|
||||
@@ -79,11 +80,11 @@ async function resolvePortable() {
|
||||
tag,
|
||||
});
|
||||
|
||||
let assets = release.assets.filter((x) => {
|
||||
const assets = release.assets.filter((x) => {
|
||||
return x.name === zipFile;
|
||||
});
|
||||
if (assets.length > 0) {
|
||||
let id = assets[0].id;
|
||||
const id = assets[0].id;
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: id,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
import fsp from "fs/promises";
|
||||
import { createRequire } from "module";
|
||||
import path from "path";
|
||||
|
||||
import AdmZip from "adm-zip";
|
||||
|
||||
const target = process.argv.slice(2)[0];
|
||||
const ARCH_MAP = {
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import AdmZip from "adm-zip";
|
||||
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 path from "path";
|
||||
import { extract } from "tar";
|
||||
import zlib from "zlib";
|
||||
|
||||
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
|
||||
*
|
||||
*/
|
||||
|
||||
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",
|
||||
@@ -43,8 +57,7 @@ const ARCH_MAP = {
|
||||
|
||||
const arg1 = process.argv.slice(2)[0];
|
||||
const arg2 = process.argv.slice(2)[1];
|
||||
let target;
|
||||
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||
const target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||
const { platform, arch } = target
|
||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||
: process;
|
||||
@@ -55,65 +68,119 @@ const SIDECAR_HOST = target
|
||||
.toString()
|
||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
/* ======= clash meta alpha======= */
|
||||
// =======================
|
||||
// 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)
|
||||
// =======================
|
||||
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_ALPHA_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v1",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"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 alpha release version from the version.txt file
|
||||
async function getLatestAlphaVersion() {
|
||||
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);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
let v = await response.text();
|
||||
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
|
||||
} 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 = {
|
||||
const META_ALPHA_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v1-go122",
|
||||
"darwin-arm64": "mihomo-darwin-arm64-go122",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
@@ -122,65 +189,116 @@ const META_MAP = {
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
async function getLatestReleaseVersion() {
|
||||
const options = {};
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2-go122",
|
||||
"darwin-arm64": "mihomo-darwin-arm64-go122",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-riscv64": "mihomo-linux-riscv64",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// =======================
|
||||
// Fetch latest versions
|
||||
// =======================
|
||||
async function getLatestAlphaVersion() {
|
||||
if (!FORCE) {
|
||||
const cached = await getCachedVersion("META_ALPHA_VERSION");
|
||||
if (cached) {
|
||||
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();
|
||||
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);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
const response = await fetch(META_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
let v = await response.text();
|
||||
META_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${META_VERSION_URL}: ${response.status}`,
|
||||
);
|
||||
META_VERSION = (await response.text()).trim();
|
||||
log_info(`Latest release version: ${META_VERSION}`);
|
||||
} catch (error) {
|
||||
log_error("Error fetching latest release version:", error.message);
|
||||
await setCachedVersion("META_VERSION", META_VERSION);
|
||||
} catch (err) {
|
||||
log_error("Error fetching latest release version:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* check available
|
||||
*/
|
||||
// =======================
|
||||
// Validate availability
|
||||
// =======================
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
);
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
||||
}
|
||||
|
||||
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* core info
|
||||
*/
|
||||
// =======================
|
||||
// Build meta objects
|
||||
// =======================
|
||||
function clashMetaAlpha() {
|
||||
const name = META_ALPHA_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo-alpha",
|
||||
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
exeFile: `${name}${isWin ? ".exe" : ""}`,
|
||||
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
|
||||
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,35 +306,83 @@ function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo",
|
||||
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
exeFile: `${name}${isWin ? ".exe" : ""}`,
|
||||
zipFile: `${name}-${META_VERSION}.${urlExt}`,
|
||||
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* download sidecar and rename
|
||||
*/
|
||||
|
||||
// =======================
|
||||
// download helper (增强:status + magic bytes)
|
||||
// =======================
|
||||
async function downloadFile(url, outPath) {
|
||||
const options = {};
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
// 将 body 写到文件以便排查(可通过临时目录查看)
|
||||
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
||||
await fsp.writeFile(outPath, body);
|
||||
throw new Error(`Failed to download ${url}: status ${response.status}`);
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await response.arrayBuffer());
|
||||
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
||||
|
||||
// 简单 magic 字节检查
|
||||
if (url.endsWith(".gz") || url.endsWith(".tgz")) {
|
||||
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
|
||||
await fsp.writeFile(outPath, buf);
|
||||
throw new Error(
|
||||
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
|
||||
);
|
||||
}
|
||||
} else if (url.endsWith(".zip")) {
|
||||
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
|
||||
await fsp.writeFile(outPath, buf);
|
||||
throw new Error(
|
||||
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await fsp.writeFile(outPath, buf);
|
||||
log_success(`download finished: ${url}`);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// resolveSidecar (支持 zip / tgz / gz)
|
||||
// =======================
|
||||
async function resolveSidecar(binInfo) {
|
||||
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
||||
|
||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||
const sidecarPath = path.join(sidecarDir, targetFile);
|
||||
|
||||
await fsp.mkdir(sidecarDir, { recursive: true });
|
||||
if (!FORCE && fs.existsSync(sidecarPath)) return;
|
||||
|
||||
if (!FORCE && fs.existsSync(sidecarPath)) {
|
||||
log_success(`"${name}" already exists, skipping download`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempDir = path.join(TEMP_DIR, name);
|
||||
const tempZip = path.join(tempDir, zipFile);
|
||||
const tempExe = path.join(tempDir, exeFile);
|
||||
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(tempZip)) {
|
||||
await downloadFile(downloadURL, tempZip);
|
||||
@@ -225,140 +391,118 @@ async function resolveSidecar(binInfo) {
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
log_debug(`"${name}" entry name`, entry.entryName);
|
||||
log_debug(`"${name}" entry: ${entry.entryName}`);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fsp.rename(tempExe, sidecarPath);
|
||||
// 尝试按 exeFile 重命名,否则找第一个可执行文件
|
||||
if (fs.existsSync(tempExe)) {
|
||||
await fsp.rename(tempExe, sidecarPath);
|
||||
} else {
|
||||
// 搜索候选
|
||||
const files = await fsp.readdir(tempDir);
|
||||
const candidate = files.find(
|
||||
(f) =>
|
||||
f === path.basename(exeFile) ||
|
||||
f.endsWith(".exe") ||
|
||||
!f.includes("."),
|
||||
);
|
||||
if (!candidate)
|
||||
throw new Error(`Expected binary not found in ${tempDir}`);
|
||||
await fsp.rename(path.join(tempDir, candidate), sidecarPath);
|
||||
}
|
||||
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`unzip finished: "${name}"`);
|
||||
} else if (zipFile.endsWith(".tgz")) {
|
||||
// tgz
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
await extract({
|
||||
cwd: tempDir,
|
||||
file: tempZip,
|
||||
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||
});
|
||||
await extract({ cwd: tempDir, file: tempZip });
|
||||
const files = await fsp.readdir(tempDir);
|
||||
log_debug(`"${name}" files in tempDir:`, files);
|
||||
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
|
||||
if (extractedFile) {
|
||||
const extractedFilePath = path.join(tempDir, extractedFile);
|
||||
await fsp.rename(extractedFilePath, sidecarPath);
|
||||
log_success(`"${name}" file renamed to "${sidecarPath}"`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`chmod binary finished: "${name}"`);
|
||||
} else {
|
||||
throw new Error(`Expected file not found in ${tempDir}`);
|
||||
}
|
||||
log_debug(`"${name}" extracted files:`, files);
|
||||
// 优先寻找给定 exeFile 或已知前缀
|
||||
let extracted = files.find(
|
||||
(f) =>
|
||||
f === path.basename(exeFile) ||
|
||||
f.startsWith("虚空终端-") ||
|
||||
!f.includes("."),
|
||||
);
|
||||
if (!extracted) extracted = files[0];
|
||||
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`);
|
||||
await fsp.rename(path.join(tempDir, extracted), sidecarPath);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`tgz processed: "${name}"`);
|
||||
} else {
|
||||
// gz
|
||||
// .gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
await new Promise((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
log_error(`"${name}" gz failed:`, error.message);
|
||||
reject(error);
|
||||
};
|
||||
readStream
|
||||
.pipe(zlib.createGunzip().on("error", onError))
|
||||
.pipe(zlib.createGunzip())
|
||||
.on("error", (e) => {
|
||||
log_error(`gunzip error for ${name}:`, e.message);
|
||||
reject(e);
|
||||
})
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`chmod binary finished: "${name}"`);
|
||||
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
|
||||
resolve();
|
||||
})
|
||||
.on("error", onError);
|
||||
.on("error", (e) => {
|
||||
log_error(`write stream error for ${name}:`, e.message);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
log_success(`gz binary processed: "${name}"`);
|
||||
}
|
||||
} catch (err) {
|
||||
// 需要删除文件
|
||||
await fsp.rm(sidecarPath, { recursive: true, force: true });
|
||||
throw err;
|
||||
} finally {
|
||||
// delete temp dir
|
||||
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const resolveSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "set_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||
});
|
||||
const resolveUnSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "unset_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||
});
|
||||
|
||||
/**
|
||||
* download the file to the resources dir
|
||||
*/
|
||||
async function resolveResource(binInfo) {
|
||||
const { file, downloadURL, localPath } = binInfo;
|
||||
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
const targetPath = path.join(resDir, file);
|
||||
|
||||
if (!FORCE && fs.existsSync(targetPath)) return;
|
||||
if (!FORCE && fs.existsSync(targetPath) && !downloadURL && !localPath) {
|
||||
log_success(`"${file}" already exists, skipping`);
|
||||
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) {
|
||||
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}"`);
|
||||
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}`);
|
||||
}
|
||||
|
||||
log_success(`${file} finished`);
|
||||
}
|
||||
|
||||
/**
|
||||
* download file and save to `path`
|
||||
*/ async function downloadFile(url, path) {
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
const buffer = await response.arrayBuffer();
|
||||
await fsp.writeFile(path, new Uint8Array(buffer));
|
||||
|
||||
log_success(`download finished: ${url}`);
|
||||
}
|
||||
|
||||
// SimpleSC.dll
|
||||
// SimpleSC.dll (win plugin)
|
||||
const resolvePlugin = async () => {
|
||||
const url =
|
||||
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
|
||||
|
||||
const tempDir = path.join(TEMP_DIR, "SimpleSC");
|
||||
const tempZip = path.join(
|
||||
tempDir,
|
||||
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
|
||||
);
|
||||
const tempDll = path.join(tempDir, "SimpleSC.dll");
|
||||
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
|
||||
const pluginDir = path.join(process.env.APPDATA || "", "Local/NSIS");
|
||||
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
|
||||
await fsp.mkdir(pluginDir, { recursive: true });
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
@@ -368,18 +512,33 @@ const resolvePlugin = async () => {
|
||||
await downloadFile(url, tempZip);
|
||||
}
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
log_debug(`"SimpleSC" entry name`, entry.entryName);
|
||||
});
|
||||
zip
|
||||
.getEntries()
|
||||
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName));
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||
log_success(`unzip finished: "SimpleSC"`);
|
||||
if (fs.existsSync(tempDll)) {
|
||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||
log_success(`unzip finished: "SimpleSC"`);
|
||||
} else {
|
||||
// 如果 dll 名称不同,尝试找到 dll
|
||||
const files = await fsp.readdir(tempDir);
|
||||
const dll = files.find((f) => f.toLowerCase().endsWith(".dll"));
|
||||
if (dll) {
|
||||
await fsp.cp(path.join(tempDir, dll), pluginPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
log_success(`unzip finished: "SimpleSC" (found ${dll})`);
|
||||
} else {
|
||||
throw new Error("SimpleSC.dll not found in zip");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
// service chmod
|
||||
// service chmod (保留并使用 glob)
|
||||
const resolveServicePermission = async () => {
|
||||
const serviceExecutables = [
|
||||
"clash-verge-service*",
|
||||
@@ -387,74 +546,59 @@ const resolveServicePermission = async () => {
|
||||
"clash-verge-service-uninstall*",
|
||||
];
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
for (let f of serviceExecutables) {
|
||||
// 使用glob模块来处理通配符
|
||||
const hashCache = await loadHashCache();
|
||||
let hasChanges = false;
|
||||
|
||||
for (const f of serviceExecutables) {
|
||||
const files = glob.sync(path.join(resDir, f));
|
||||
for (let filePath of files) {
|
||||
for (const filePath of files) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
execSync(`chmod 755 ${filePath}`);
|
||||
log_success(`chmod finished: "${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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await saveHashCache(hashCache);
|
||||
}
|
||||
};
|
||||
|
||||
// 在 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
|
||||
*/
|
||||
// =======================
|
||||
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
|
||||
// =======================
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
resolveResource({
|
||||
const ext = platform === "win32" ? ".exe" : "";
|
||||
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
file: "clash-verge-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveInstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
resolveResource({
|
||||
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}`,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveUninstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
|
||||
resolveResource({
|
||||
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}`,
|
||||
});
|
||||
@@ -480,15 +624,27 @@ const resolveEnableLoopback = () =>
|
||||
file: "enableLoopback.exe",
|
||||
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
|
||||
});
|
||||
|
||||
const resolveWinSysproxy = () =>
|
||||
resolveResource({
|
||||
file: "sysproxy.exe",
|
||||
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
|
||||
});
|
||||
|
||||
const resolveSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "set_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||
});
|
||||
const resolveUnSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "unset_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||
});
|
||||
|
||||
// =======================
|
||||
// Tasks
|
||||
// =======================
|
||||
const tasks = [
|
||||
// { name: "clash", func: resolveClash, retry: 5 },
|
||||
{
|
||||
name: "verge-mihomo-alpha",
|
||||
func: () =>
|
||||
@@ -538,11 +694,6 @@ const tasks = [
|
||||
retry: 5,
|
||||
macosOnly: true,
|
||||
},
|
||||
{
|
||||
name: "locales",
|
||||
func: resolveLocales,
|
||||
retry: 2,
|
||||
},
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
|
||||
@@ -30,10 +30,11 @@
|
||||
*/
|
||||
|
||||
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}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { readFileSync } from "fs";
|
||||
import { log_success, log_error, log_info } from "./utils.mjs";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { log_error, log_info, log_success } from "./utils.mjs";
|
||||
|
||||
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
||||
const CHAT_ID_TEST = "@vergetest"; // 测试频道
|
||||
@@ -71,6 +73,19 @@ async function sendTelegramNotification() {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function normalizeDetailsTags(content) {
|
||||
return content
|
||||
.replace(
|
||||
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
|
||||
"\n<b>$1</b>\n",
|
||||
)
|
||||
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
|
||||
.replace(/<\/?details>/g, "")
|
||||
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
|
||||
.replace(/<br\s*\/?>/g, "\n");
|
||||
}
|
||||
|
||||
releaseContent = normalizeDetailsTags(releaseContent);
|
||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
|
||||
@@ -2,9 +2,9 @@ import fs from "fs";
|
||||
import fsp from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const UPDATE_LOG = "UPDATELOG.md";
|
||||
const UPDATE_LOG = "Changelog.md";
|
||||
|
||||
// parse the UPDATELOG.md
|
||||
// parse the Changelog.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 UPDATELOG.md");
|
||||
throw new Error("could not found Changelog.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 UPDATELOG.md`);
|
||||
throw new Error(`could not found "${tag}" in Changelog.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 UPDATELOG.md");
|
||||
throw new Error("could not found Changelog.md");
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(file, "utf-8");
|
||||
@@ -58,7 +58,7 @@ export async function resolveUpdateLogDefault() {
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
let content = [];
|
||||
const 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 UPDATELOG.md");
|
||||
throw new Error("could not found any version tag in Changelog.md");
|
||||
}
|
||||
|
||||
return content.join("\n").trim();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -35,7 +36,7 @@ async function resolveUpdater() {
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
||||
notes: await resolveUpdateLog(tag.name), // use Changelog.md
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
"windows-x86_64": { signature: "", url: "" },
|
||||
@@ -113,7 +114,7 @@ async function resolveUpdater() {
|
||||
});
|
||||
|
||||
// delete the old assets
|
||||
for (let asset of updateRelease.assets) {
|
||||
for (const asset of updateRelease.assets) {
|
||||
if (asset.name === UPDATE_JSON_FILE) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
|
||||
// Add stable update JSON filenames
|
||||
@@ -259,7 +260,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 (let asset of updateRelease.assets) {
|
||||
for (const asset of updateRelease.assets) {
|
||||
if (asset.name === jsonFile) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
avoid-breaking-exported-api = true
|
||||
avoid-breaking-exported-api = true
|
||||
cognitive-complexity-threshold = 25
|
||||
Generated
+1103
-1003
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -8,42 +8,42 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
rust-version = "1.91"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.4.1", features = [] }
|
||||
tauri-build = { version = "2.5.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
anyhow = "1.0.100"
|
||||
dirs = "6.0"
|
||||
open = "5.3.2"
|
||||
log = "0.4.28"
|
||||
dunce = "1.0.5"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.42"
|
||||
sysinfo = { version = "0.37.2", features = ["network", "system"] }
|
||||
boa_engine = "0.20.0"
|
||||
boa_engine = "0.21.0"
|
||||
serde_json = "1.0.145"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
once_cell = "1.21.3"
|
||||
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12.5"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
percent-encoding = "2.3.2"
|
||||
tokio = { version = "1.47.1", features = [
|
||||
tokio = { version = "1.48.0", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||
regex = "1.12.1"
|
||||
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
|
||||
regex = "1.12.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
tauri = { version = "2.8.5", features = [
|
||||
tauri = { version = "2.9.3", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -51,46 +51,40 @@ tauri = { version = "2.8.5", features = [
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
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"
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-deep-link = "2.4.5"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
zip = "6.0.0"
|
||||
reqwest_dav = "0.2.2"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.3"
|
||||
getrandom = "0.3.4"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
libc = "0.2.177"
|
||||
gethostname = "1.0.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
dashmap = "6.1.0"
|
||||
tauri-plugin-notification = "2.3.1"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
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.2"
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
tauri-plugin-http = "2.5.4"
|
||||
flexi_logger = "0.31.7"
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
console-subscriber = { version = "0.5.0", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||
clash_verge_logger = { version = "0.1.0", git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
clash_verge_service_ipc = { version = "2.0.14", features = [
|
||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
async-trait = "0.1.89"
|
||||
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||
clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
# clash_verge_service_ipc = { version = "2.0.14", features = [
|
||||
# "client",
|
||||
# ], path = "../../clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
rust-i18n = "3.1.5"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -109,13 +103,19 @@ winapi = { version = "0.3.9", features = [
|
||||
"winhttp",
|
||||
"winreg",
|
||||
] }
|
||||
windows-sys = { version = "0.61.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
users = "0.11.0"
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook = "0.3.18"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
tauri-plugin-global-shortcut = "2.3.0"
|
||||
tauri-plugin-autostart = "2.5.1"
|
||||
tauri-plugin-global-shortcut = "2.3.1"
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
@@ -124,12 +124,19 @@ custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = ["clash_verge_logger/color"]
|
||||
tauri-dev = []
|
||||
tokio-trace = ["console-subscriber"]
|
||||
clippy = ["tauri/test"]
|
||||
tracing = []
|
||||
|
||||
[[bench]]
|
||||
name = "draft_benchmark"
|
||||
path = "benches/draft_benchmark.rs"
|
||||
harness = false
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 16
|
||||
codegen-units = 1
|
||||
lto = "thin"
|
||||
opt-level = 2
|
||||
opt-level = 3
|
||||
debug = false
|
||||
strip = true
|
||||
overflow-checks = false
|
||||
@@ -147,8 +154,8 @@ rpath = false
|
||||
|
||||
[profile.fast-release]
|
||||
inherits = "release"
|
||||
incremental = true
|
||||
codegen-units = 64
|
||||
incremental = true
|
||||
lto = false
|
||||
opt-level = 0
|
||||
debug = true
|
||||
@@ -159,7 +166,7 @@ name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.7.0"
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
|
||||
[lints.clippy]
|
||||
# Core categories - most important for code safety and correctness
|
||||
@@ -175,8 +182,10 @@ unimplemented = "deny"
|
||||
# Development quality lints
|
||||
todo = "warn"
|
||||
dbg_macro = "warn"
|
||||
#print_stdout = "warn"
|
||||
#print_stderr = "warn"
|
||||
|
||||
# 我们期望所有输出方式通过 logging 模块进行统一管理
|
||||
# print_stdout = "deny"
|
||||
# print_stderr = "deny"
|
||||
|
||||
# Performance lints for proxy application
|
||||
clone_on_ref_ptr = "warn"
|
||||
@@ -211,3 +220,37 @@ needless_raw_string_hashes = "deny" # Too many in existing code
|
||||
#pedantic = { level = "allow", priority = -1 }
|
||||
#nursery = { level = "allow", priority = -1 }
|
||||
#restriction = { level = "allow", priority = -1 }
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,91 +1,116 @@
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use std::hint::black_box;
|
||||
use std::process;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
// 业务模型 & Draft
|
||||
use app_lib::config::Draft as DraftNew;
|
||||
use app_lib::config::IVerge;
|
||||
use app_lib::utils::Draft as DraftNew;
|
||||
|
||||
// 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()
|
||||
// });
|
||||
/// 创建测试数据
|
||||
fn make_draft() -> DraftNew<IVerge> {
|
||||
let verge = IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
DraftNew::new(verge)
|
||||
}
|
||||
|
||||
// let draft = DraftOld::from(black_box(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 d = draft.draft_mut();
|
||||
// d.enable_auto_launch = Some(false);
|
||||
// }
|
||||
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));
|
||||
|
||||
// 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| {
|
||||
group.bench_function("data_mut", |b| {
|
||||
b.iter(|| {
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..Default::default()
|
||||
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 draft = DraftNew::from(black_box(verge));
|
||||
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
|
||||
let _ = draft.apply();
|
||||
let latest2 = draft.latest_arc();
|
||||
black_box(&latest2.enable_tun_mode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:修改草稿并 discard()
|
||||
fn bench_discard_new(c: &mut Criterion) {
|
||||
c.bench_function("discard_draft_new", |b| {
|
||||
group.bench_function("latest_arc", |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();
|
||||
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_apply_old,
|
||||
// bench_discard_old,
|
||||
bench_apply_new,
|
||||
bench_discard_new
|
||||
);
|
||||
criterion_group!(benches, bench_draft);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
#[cfg(feature = "clippy")]
|
||||
{
|
||||
println!("cargo:warning=Skipping tauri_build during Clippy");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "clippy"))]
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: 대시보드
|
||||
body: 대시보드 표시 상태가 업데이트되었습니다.
|
||||
clashModeChanged:
|
||||
title: 모드 전환
|
||||
body: "{mode}(으)로 전환되었습니다."
|
||||
systemProxyToggled:
|
||||
title: 시스템 프록시
|
||||
body: 시스템 프록시 상태가 업데이트되었습니다.
|
||||
tunModeToggled:
|
||||
title: TUN 모드
|
||||
body: TUN 모드 상태가 업데이트되었습니다.
|
||||
lightweightModeEntered:
|
||||
title: 경량 모드
|
||||
body: 경량 모드에 진입했습니다.
|
||||
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: 프로필
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_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.
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: 仪表板
|
||||
body: 仪表板显示状态已更新。
|
||||
clashModeChanged:
|
||||
title: 模式切换
|
||||
body: 已切换至 {mode}。
|
||||
systemProxyToggled:
|
||||
title: 系统代理
|
||||
body: 系统代理状态已更新。
|
||||
tunModeToggled:
|
||||
title: TUN 模式
|
||||
body: TUN 模式状态已更新。
|
||||
lightweightModeEntered:
|
||||
title: 轻量模式
|
||||
body: 已进入轻量模式。
|
||||
appQuit:
|
||||
title: 即将退出
|
||||
body: Clash Verge 即将退出。
|
||||
appHidden:
|
||||
title: 应用已隐藏
|
||||
body: Clash Verge 正在后台运行。
|
||||
service:
|
||||
adminPrompt: 安装服务需要管理员权限
|
||||
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: 订阅
|
||||
@@ -0,0 +1,56 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: 儀表板
|
||||
body: 儀表板顯示狀態已更新。
|
||||
clashModeChanged:
|
||||
title: 模式切換
|
||||
body: 已切換至 {mode}。
|
||||
systemProxyToggled:
|
||||
title: 系統代理
|
||||
body: 系統代理狀態已更新。
|
||||
tunModeToggled:
|
||||
title: 虛擬網路介面卡模式
|
||||
body: 已更新虛擬網路介面卡模式狀態。
|
||||
lightweightModeEntered:
|
||||
title: 輕量模式
|
||||
body: 已進入輕量模式。
|
||||
appQuit:
|
||||
title: 即將退出
|
||||
body: Clash Verge 即將退出。
|
||||
appHidden:
|
||||
title: 應用已隱藏
|
||||
body: Clash Verge 正在背景執行。
|
||||
service:
|
||||
adminPrompt: 安裝服務需要管理員權限
|
||||
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: 訂閱
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.91.0"
|
||||
components = ["rustfmt", "clippy"]
|
||||
@@ -6,7 +6,7 @@ use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
|
||||
@@ -1,37 +1,60 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::sysopt::Sysopt;
|
||||
use crate::utils::resolve::ui::{self, UiReadyStage};
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
feat, logging,
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
utils::{
|
||||
dirs::{self, PathBufExec as _},
|
||||
logging::Type,
|
||||
},
|
||||
};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
use tauri::{AppHandle, Manager as _};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
|
||||
/// 打开应用程序所在目录
|
||||
#[tauri::command]
|
||||
pub async fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
open::that(app_dir).stringify_err()
|
||||
}
|
||||
|
||||
/// 打开核心所在目录
|
||||
#[tauri::command]
|
||||
pub async fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
open::that(core_dir).stringify_err()
|
||||
}
|
||||
|
||||
/// 打开日志目录
|
||||
#[tauri::command]
|
||||
pub async fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
let log_dir = dirs::app_logs_dir().stringify_err()?;
|
||||
open::that(log_dir).stringify_err()
|
||||
}
|
||||
|
||||
/// 打开网页链接
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
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()
|
||||
}
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
@@ -68,36 +91,39 @@ pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
/// 获取应用目录
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||
let app_home_dir = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
.into();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
/// 获取当前自启动状态
|
||||
#[tauri::command]
|
||||
pub fn get_auto_launch_status() -> CmdResult<bool> {
|
||||
use crate::core::sysopt::Sysopt;
|
||||
wrap_err!(Sysopt::global().get_launch_status())
|
||||
Sysopt::global().get_launch_status().stringify_err()
|
||||
}
|
||||
|
||||
/// 下载图标缓存
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||
let icon_path = icon_cache_dir.join(&name);
|
||||
let icon_cache_dir = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("icons")
|
||||
.join("cache");
|
||||
let icon_path = icon_cache_dir.join(name.as_str());
|
||||
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
return Ok(icon_path.to_string_lossy().into());
|
||||
}
|
||||
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
let _ = fs::create_dir_all(&icon_cache_dir).await;
|
||||
}
|
||||
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str()));
|
||||
|
||||
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||
let response = reqwest::get(url.as_str()).await.stringify_err()?;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
@@ -107,7 +133,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
|
||||
let is_image = content_type.starts_with("image/");
|
||||
|
||||
let content = wrap_err!(response.bytes().await)?;
|
||||
let content = response.bytes().await.stringify_err()?;
|
||||
|
||||
let is_html = content.len() > 15
|
||||
&& (content.starts_with(b"<!DOCTYPE html")
|
||||
@@ -116,37 +142,37 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
|
||||
if is_image && !is_html {
|
||||
{
|
||||
let mut file = match std::fs::File::create(&temp_path) {
|
||||
let mut file = match fs::File::create(&temp_path).await {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
return Ok(icon_path.to_string_lossy().into());
|
||||
}
|
||||
return Err("Failed to create temporary file".into());
|
||||
}
|
||||
};
|
||||
|
||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||
file.write_all(content.as_ref()).await.stringify_err()?;
|
||||
file.flush().await.stringify_err()?;
|
||||
}
|
||||
|
||||
if !icon_path.exists() {
|
||||
match std::fs::rename(&temp_path, &icon_path) {
|
||||
match fs::rename(&temp_path, &icon_path).await {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
return Ok(icon_path.to_string_lossy().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
}
|
||||
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
Ok(icon_path.to_string_lossy().into())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {url}"))
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
Err(format!("下载的内容不是有效图片: {}", url.as_str()).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,34 +185,43 @@ pub struct IconInfo {
|
||||
|
||||
/// 复制图标文件
|
||||
#[tauri::command]
|
||||
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
use std::{fs, path::Path};
|
||||
pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
let file_path = Path::new(path.as_str());
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||
let icon_dir = dirs::app_home_dir().stringify_err()?.join("icons");
|
||||
if !icon_dir.exists() {
|
||||
let _ = fs::create_dir_all(&icon_dir);
|
||||
let _ = fs::create_dir_all(&icon_dir).await;
|
||||
}
|
||||
let ext = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().to_string(),
|
||||
None => "ico".to_string(),
|
||||
let ext: String = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().into(),
|
||||
None => "ico".into(),
|
||||
};
|
||||
|
||||
let dest_path = icon_dir.join(format!(
|
||||
"{0}-{1}.{ext}",
|
||||
icon_info.name, icon_info.current_t
|
||||
icon_info.name.as_str(),
|
||||
icon_info.current_t.as_str()
|
||||
));
|
||||
if file_path.exists() {
|
||||
if icon_info.previous_t.trim() != "" {
|
||||
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();
|
||||
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();
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
@@ -195,42 +230,27 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
path,
|
||||
dest_path
|
||||
);
|
||||
match fs::copy(file_path, &dest_path) {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
match fs::copy(file_path, &dest_path).await {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().into()),
|
||||
Err(err) => Err(err.to_string().into()),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".to_string())
|
||||
Err("file not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
log::info!(target: "app", "前端UI已准备就绪");
|
||||
crate::utils::resolve::ui::mark_ui_ready();
|
||||
logging!(info, Type::Cmd, "前端UI已准备就绪");
|
||||
ui::mark_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
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);
|
||||
pub fn update_ui_stage(stage: UiReadyStage) -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage);
|
||||
ui::update_ui_ready_stage(stage);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::utils::dirs;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{self, CoreManager, RunningMode, handle, logger},
|
||||
cmd::StringifyErr as _,
|
||||
config::{ClashInfo, Config},
|
||||
constants,
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
};
|
||||
use crate::{config::*, feat, logging, utils::logging::Type, wrap_err};
|
||||
use crate::{feat, logging, utils::logging::Type};
|
||||
use compact_str::CompactString;
|
||||
use serde_yaml_ng::Mapping;
|
||||
// use std::time::Duration;
|
||||
|
||||
// const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
@@ -21,13 +22,13 @@ pub async fn copy_clash_env() -> CmdResult {
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().await.latest_ref().get_client_info())
|
||||
Ok(Config::clash().await.data_arc().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
feat::patch_clash(payload).await.stringify_err()
|
||||
}
|
||||
|
||||
/// 修改Clash模式
|
||||
@@ -42,10 +43,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global()
|
||||
.change_core(Some(clash_core.clone()))
|
||||
.await
|
||||
{
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
Ok(_) => {
|
||||
// 切换内核后重启内核
|
||||
match CoreManager::global().restart_core().await {
|
||||
@@ -55,22 +53,23 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
Type::Core,
|
||||
"core changed and restarted to {clash_core}"
|
||||
);
|
||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||
handle::Handle::notice_message("config_core::change_success", clash_core);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Core changed but failed to restart: {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());
|
||||
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 = err.to_string();
|
||||
let error_msg: String = err;
|
||||
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
}
|
||||
@@ -79,7 +78,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
/// 启动核心
|
||||
#[tauri::command]
|
||||
pub async fn start_core() -> CmdResult {
|
||||
let result = wrap_err!(CoreManager::global().start_core().await);
|
||||
let result = CoreManager::global().start_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
@@ -89,7 +88,7 @@ pub async fn start_core() -> CmdResult {
|
||||
/// 关闭核心
|
||||
#[tauri::command]
|
||||
pub async fn stop_core() -> CmdResult {
|
||||
let result = wrap_err!(CoreManager::global().stop_core().await);
|
||||
let result = CoreManager::global().stop_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
@@ -99,7 +98,7 @@ pub async fn stop_core() -> CmdResult {
|
||||
/// 重启核心
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
let result = wrap_err!(CoreManager::global().restart_core().await);
|
||||
let result = CoreManager::global().restart_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
@@ -112,7 +111,7 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
10000u32
|
||||
}
|
||||
};
|
||||
@@ -128,14 +127,12 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
|
||||
// 获取DNS配置文件路径
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
// 保存DNS配置到文件
|
||||
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())?;
|
||||
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
|
||||
fs::write(&dns_path, yaml_str).await.stringify_err()?;
|
||||
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||
|
||||
Ok(())
|
||||
@@ -144,33 +141,25 @@ 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()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if !dns_path.exists() {
|
||||
logging!(warn, Type::Config, "DNS config file not found");
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
|
||||
let dns_yaml = tokio::fs::read_to_string(&dns_path).await.map_err(|e| {
|
||||
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
|
||||
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
// 解析DNS配置
|
||||
let patch_config =
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).map_err(|e| {
|
||||
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}");
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "Applying DNS config from file");
|
||||
@@ -180,30 +169,26 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
patch.insert("dns".into(), patch_config.into());
|
||||
|
||||
// 应用DNS配置到运行时配置
|
||||
Config::runtime().await.draft_mut().patch_config(patch);
|
||||
Config::runtime().await.edit_draft(|d| {
|
||||
d.patch_config(patch);
|
||||
});
|
||||
|
||||
// 重新生成配置
|
||||
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()
|
||||
Config::generate().await.stringify_err_log(|err| {
|
||||
let err = format!("Failed to regenerate 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()
|
||||
})?;
|
||||
CoreManager::global()
|
||||
.update_config()
|
||||
.await
|
||||
.stringify_err_log(|err| {
|
||||
let err = format!("Failed to apply config with DNS: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
} else {
|
||||
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
|
||||
logging!(
|
||||
@@ -212,24 +197,23 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
"DNS settings disabled, regenerating config"
|
||||
);
|
||||
|
||||
Config::generate().await.map_err(|err| {
|
||||
logging!(error, Type::Config, "Failed to regenerate config: {err}");
|
||||
"Failed to regenerate config".to_string()
|
||||
Config::generate().await.stringify_err_log(|err| {
|
||||
let err = format!("Failed to regenerate 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()
|
||||
})?;
|
||||
CoreManager::global()
|
||||
.update_config()
|
||||
.await
|
||||
.stringify_err_log(|err| {
|
||||
let err = format!("Failed to apply regenerated config: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -239,8 +223,8 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
use crate::utils::dirs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
Ok(dns_path.exists())
|
||||
}
|
||||
@@ -252,48 +236,38 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
use tokio::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
|
||||
if !fs::try_exists(&dns_path).await.map_err(|e| e.to_string())? {
|
||||
if !fs::try_exists(&dns_path).await.stringify_err()? {
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&dns_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let content = fs::read_to_string(&dns_path).await.stringify_err()?.into();
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// 验证DNS配置文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
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 app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
let dns_path_str = dns_path.to_str().unwrap_or_default();
|
||||
|
||||
if !dns_path.exists() {
|
||||
return Ok((false, "DNS config file not found".to_string()));
|
||||
return Ok((false, "DNS config file not found".into()));
|
||||
}
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(dns_path_str, None)
|
||||
CoreConfigValidator::validate_config_file(dns_path_str, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||
let logs = match core::CoreManager::global().get_running_mode() {
|
||||
// TODO: 服务模式下日志获取接口
|
||||
RunningMode::Service => VecDeque::new(),
|
||||
RunningMode::Sidecar => logger::Logger::global().get_logs().clone(),
|
||||
_ => VecDeque::new(),
|
||||
};
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<CompactString>> {
|
||||
let logs = CoreManager::global()
|
||||
.get_clash_logs()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
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()
|
||||
.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()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
let url_country = "https://chat.openai.com/cdn-cgi/trace";
|
||||
let result_country = client.get(url_country).send().await;
|
||||
|
||||
let region = match result_country {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let mut map = HashMap::new();
|
||||
for line in body.lines() {
|
||||
if let Some(index) = line.find('=') {
|
||||
let key = &line[..index];
|
||||
let value = &line[index + 1..];
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
map.get("loc").map(|loc| {
|
||||
let emoji = country_code_to_emoji(loc);
|
||||
format!("{emoji}{loc}")
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let url_ios = "https://ios.chat.openai.com/";
|
||||
let result_ios = client.get(url_ios).send().await;
|
||||
|
||||
let ios_status = match result_ios {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("you may be connected to a disallowed isp") {
|
||||
"Disallowed ISP"
|
||||
} else if body_lower.contains("request is not allowed. please try again later.") {
|
||||
"Yes"
|
||||
} else if body_lower.contains("sorry, you have been blocked") {
|
||||
"Blocked"
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
}
|
||||
Err(_) => "Failed",
|
||||
};
|
||||
|
||||
let url_web = "https://api.openai.com/compliance/cookie_requirements";
|
||||
let result_web = client.get(url_web).send().await;
|
||||
|
||||
let web_status = match result_web {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("unsupported_country") {
|
||||
"Unsupported Country/Region"
|
||||
} else {
|
||||
"Yes"
|
||||
}
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
}
|
||||
Err(_) => "Failed",
|
||||
};
|
||||
|
||||
results.push(UnlockItem {
|
||||
name: "ChatGPT iOS".to_string(),
|
||||
status: ios_status.to_string(),
|
||||
region: region.clone(),
|
||||
check_time: Some(get_local_date_string()),
|
||||
});
|
||||
|
||||
results.push(UnlockItem {
|
||||
name: "ChatGPT Web".to_string(),
|
||||
status: web_status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
});
|
||||
|
||||
results
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const BLOCKED_CODES: [&str; 10] = ["AF", "BY", "CN", "CU", "HK", "IR", "KP", "MO", "RU", "SY"];
|
||||
|
||||
pub(super) async fn check_claude(client: &Client) -> UnlockItem {
|
||||
let url = "https://claude.ai/cdn-cgi/trace";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => {
|
||||
let mut country_code: Option<String> = None;
|
||||
|
||||
for line in body.lines() {
|
||||
if let Some(rest) = line.strip_prefix("loc=") {
|
||||
country_code = Some(rest.trim().to_uppercase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(code) = country_code {
|
||||
let emoji = country_code_to_emoji(&code);
|
||||
let status = if BLOCKED_CODES.contains(&code.as_str()) {
|
||||
"No"
|
||||
} else {
|
||||
"Yes"
|
||||
};
|
||||
|
||||
UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: status.to_string(),
|
||||
region: Some(format!("{emoji}{code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
|
||||
let auth_header =
|
||||
"Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
|
||||
|
||||
let device_req_body = serde_json::json!({
|
||||
"deviceFamily": "browser",
|
||||
"applicationRuntime": "chrome",
|
||||
"deviceProfile": "windows",
|
||||
"attributes": {}
|
||||
});
|
||||
|
||||
let device_result = client
|
||||
.post(device_api_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("content-type", "application/json; charset=UTF-8")
|
||||
.json(&device_req_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if device_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let device_response = match device_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Disney+ device response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if device_response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No (IP Banned By Disney+)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let device_body = match device_response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot read response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile assertion regex for Disney+: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let assertion = match re.captures(&device_body) {
|
||||
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
if assertion.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot extract assertion)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
||||
let assertion_str = match assertion {
|
||||
Some(assertion) => assertion,
|
||||
None => {
|
||||
logging!(error, Type::Network, "No assertion found for Disney+");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (No Assertion)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let token_body = [
|
||||
(
|
||||
"grant_type",
|
||||
"urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
),
|
||||
("latitude", "0"),
|
||||
("longitude", "0"),
|
||||
("platform", "browser"),
|
||||
("subject_token", assertion_str.as_str()),
|
||||
(
|
||||
"subject_token_type",
|
||||
"urn:bamtech:params:oauth:token-type:device",
|
||||
),
|
||||
];
|
||||
|
||||
let token_result = client
|
||||
.post(token_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.form(&token_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if token_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let token_response = match token_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Disney+ token response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let token_status = token_response.status();
|
||||
|
||||
let token_body_text = match token_response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot read token response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if token_body_text.contains("forbidden-location") || token_body_text.contains("403 ERROR") {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No (IP Banned By Disney+)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let token_json: Result<serde_json::Value, _> = serde_json::from_str(&token_body_text);
|
||||
|
||||
let refresh_token = match token_json {
|
||||
Ok(json) => json
|
||||
.get("refresh_token")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
Err(_) => match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) {
|
||||
Ok(refresh_token_re) => refresh_token_re
|
||||
.captures(&token_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile refresh_token regex for Disney+: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if refresh_token.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (Error: Cannot extract refresh token, status: {}, response: {})",
|
||||
token_status.as_u16(),
|
||||
token_body_text.chars().take(100).collect::<String>() + "..."
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let graphql_url = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql";
|
||||
|
||||
let graphql_payload = format!(
|
||||
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
|
||||
refresh_token.unwrap_or_default()
|
||||
);
|
||||
|
||||
let graphql_result = client
|
||||
.post(graphql_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("content-type", "application/json")
|
||||
.body(graphql_payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if graphql_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let preview_check = client.get("https://disneyplus.com").send().await;
|
||||
|
||||
let is_unavailable = match preview_check {
|
||||
Ok(response) => {
|
||||
let url = response.url().to_string();
|
||||
url.contains("preview") || url.contains("unavailable")
|
||||
}
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
let graphql_response = match graphql_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Disney+ GraphQL response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let graphql_status = graphql_response.status();
|
||||
let graphql_body_text = match graphql_response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to read Disney+ GraphQL response text: {}",
|
||||
e
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(region) = region_from_main {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if graphql_body_text.is_empty() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (GraphQL error: empty response, status: {})",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (GraphQL error: {}, status: {})",
|
||||
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ countryCode regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region_code = region_re
|
||||
.captures(&graphql_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||
|
||||
let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ supported location regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let in_supported_location = supported_re
|
||||
.captures(&graphql_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
|
||||
|
||||
if region_code.is_none() {
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(region) = region_from_main {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let region = match region_code {
|
||||
Some(code) => code,
|
||||
None => {
|
||||
logging!(error, Type::Network, "No region code found for Disney+");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if region == "JP" {
|
||||
let emoji = country_code_to_emoji("JP");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if is_unavailable {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
match in_supported_location {
|
||||
Some(false) => {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Soon".to_string(),
|
||||
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Some(true) => {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
None => UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
let url = "https://gemini.google.com";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let is_ok = body.contains("45631641,null,true");
|
||||
let status = if is_ok { "Yes" } else { "No" };
|
||||
|
||||
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Gemini regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let 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}")
|
||||
})
|
||||
});
|
||||
|
||||
UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use reqwest::Client;
|
||||
use tauri::command;
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
mod bahamut;
|
||||
mod bilibili;
|
||||
mod chatgpt;
|
||||
mod claude;
|
||||
mod disney_plus;
|
||||
mod gemini;
|
||||
mod netflix;
|
||||
mod prime_video;
|
||||
mod spotify;
|
||||
mod tiktok;
|
||||
mod types;
|
||||
mod utils;
|
||||
mod youtube;
|
||||
|
||||
pub use types::UnlockItem;
|
||||
|
||||
use bahamut::check_bahamut_anime;
|
||||
use bilibili::{check_bilibili_china_mainland, check_bilibili_hk_mc_tw};
|
||||
use chatgpt::check_chatgpt_combined;
|
||||
use claude::check_claude;
|
||||
use disney_plus::check_disney_plus;
|
||||
use gemini::check_gemini;
|
||||
use netflix::check_netflix;
|
||||
use prime_video::check_prime_video;
|
||||
use spotify::check_spotify;
|
||||
use tiktok::check_tiktok;
|
||||
use youtube::check_youtube_premium;
|
||||
|
||||
#[command]
|
||||
pub async fn get_unlock_items() -> Result<Vec<UnlockItem>, String> {
|
||||
Ok(types::default_unlock_items())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
let client = match Client::builder()
|
||||
.user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||
};
|
||||
|
||||
let results = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut tasks = JoinSet::new();
|
||||
let client_arc = Arc::new(client);
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bilibili_china_mainland(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bilibili_hk_mc_tw(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let chatgpt_results = check_chatgpt_combined(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
results.extend(chatgpt_results);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_claude(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_gemini(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_youtube_premium(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bahamut_anime(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_netflix(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_disney_plus(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_spotify(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_tiktok(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_prime_video(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
if let Err(e) = res {
|
||||
eprintln!("任务执行失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let results = match Arc::try_unwrap(results) {
|
||||
Ok(mutex) => mutex.into_inner(),
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to unwrap results Arc, references still exist"
|
||||
);
|
||||
return Err("Failed to collect results".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
let cdn_result = check_netflix_cdn(client).await;
|
||||
if cdn_result.status == "Yes" {
|
||||
return cdn_result;
|
||||
}
|
||||
|
||||
let url1 = "https://www.netflix.com/title/81280792";
|
||||
let url2 = "https://www.netflix.com/title/70143836";
|
||||
|
||||
let result1 = client
|
||||
.get(url1)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result1 {
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let result2 = client
|
||||
.get(url2)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let status1 = match result1 {
|
||||
Ok(response) => response.status().as_u16(),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Netflix response 1: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let status2 = match result2 {
|
||||
Ok(response) => response.status().as_u16(),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Netflix response 2: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if status1 == 404 && status2 == 404 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Originals Only".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if status1 == 403 || status2 == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if status1 == 200 || status1 == 301 || status2 == 200 || status2 == 301 {
|
||||
let test_url = "https://www.netflix.com/title/80018499";
|
||||
match client
|
||||
.get(test_url)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if let Some(location) = response.headers().get("location")
|
||||
&& let Ok(location_str) = location.to_str()
|
||||
{
|
||||
let parts: Vec<&str> = location_str.split('/').collect();
|
||||
if parts.len() >= 4 {
|
||||
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
||||
let emoji = country_code_to_emoji(region_code);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region_code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let emoji = country_code_to_emoji("us");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{}", "us")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("获取Netflix区域信息失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5";
|
||||
|
||||
match client
|
||||
.get(url)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No (IP Banned By Netflix)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
match response.json::<Value>().await {
|
||||
Ok(data) => {
|
||||
if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
|
||||
&& !targets.is_empty()
|
||||
&& let Some(location) = targets[0].get("location")
|
||||
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
|
||||
{
|
||||
let emoji = country_code_to_emoji(country);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{country}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Unknown".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("解析Fast.com API响应失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fast.com API请求失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.primevideo.com";
|
||||
|
||||
let result = client.get(url).send().await;
|
||||
|
||||
if result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Prime Video response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match response.text().await {
|
||||
Ok(body) => {
|
||||
let is_blocked = body.contains("isServiceRestricted");
|
||||
|
||||
let region_re = match Regex::new(r#""currentTerritory":"([^"]+)""#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Prime Video region regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region_code = region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||
|
||||
if is_blocked {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "No (Service Not Available)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(region) = region_code {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if !is_blocked {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: PAGE ERROR)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: Unknown Region)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: Cannot read response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use reqwest::{Client, Url};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_spotify(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.spotify.com/api/content/v1/country-selector?platform=web&format=json";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
let final_url = response.url().clone();
|
||||
let status_code = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
let region = extract_region(&final_url).or_else(|| extract_region_from_body(&body));
|
||||
let status = determine_status(status_code.as_u16(), &body);
|
||||
|
||||
UnlockItem {
|
||||
name: "Spotify".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Spotify".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_status(status: u16, body: &str) -> &'static str {
|
||||
if status == 403 || status == 451 {
|
||||
return "No";
|
||||
}
|
||||
|
||||
if !(200..300).contains(&status) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("not available in your country") {
|
||||
return "No";
|
||||
}
|
||||
|
||||
"Yes"
|
||||
}
|
||||
|
||||
fn extract_region(url: &Url) -> Option<String> {
|
||||
let mut segments = url.path_segments()?;
|
||||
let first_segment = segments.next()?;
|
||||
|
||||
if first_segment.is_empty() || first_segment == "api" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let country_code = first_segment.split('-').next().unwrap_or(first_segment);
|
||||
let upper = country_code.to_uppercase();
|
||||
let emoji = country_code_to_emoji(&upper);
|
||||
Some(format!("{emoji}{upper}"))
|
||||
}
|
||||
|
||||
fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
let marker = "\"countryCode\":\"";
|
||||
if let Some(idx) = body.find(marker) {
|
||||
let start = idx + marker.len();
|
||||
let rest = &body[start..];
|
||||
if let Some(end) = rest.find('"') {
|
||||
let code = rest[..end].to_uppercase();
|
||||
if !code.is_empty() {
|
||||
let emoji = country_code_to_emoji(&code);
|
||||
return Some(format!("{emoji}{code}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_tiktok(client: &Client) -> UnlockItem {
|
||||
let trace_url = "https://www.tiktok.com/cdn-cgi/trace";
|
||||
|
||||
let mut status = String::from("Failed");
|
||||
let mut region = None;
|
||||
|
||||
if let Ok(response) = client.get(trace_url).send().await {
|
||||
let status_code = response.status().as_u16();
|
||||
if let Ok(body) = response.text().await {
|
||||
status = determine_status(status_code, &body).to_string();
|
||||
region = extract_region_from_body(&body);
|
||||
}
|
||||
}
|
||||
|
||||
if (region.is_none() || status == "Failed")
|
||||
&& let Ok(response) = client.get("https://www.tiktok.com/").send().await
|
||||
{
|
||||
let status_code = response.status().as_u16();
|
||||
if let Ok(body) = response.text().await {
|
||||
let fallback_status = determine_status(status_code, &body);
|
||||
let fallback_region = extract_region_from_body(&body);
|
||||
|
||||
if status != "No" {
|
||||
status = fallback_status.to_string();
|
||||
}
|
||||
|
||||
if region.is_none() {
|
||||
region = fallback_region;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "TikTok".to_string(),
|
||||
status,
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_status(status: u16, body: &str) -> &'static str {
|
||||
if status == 403 || status == 451 {
|
||||
return "No";
|
||||
}
|
||||
|
||||
if !(200..300).contains(&status) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("access denied")
|
||||
|| body_lower.contains("not available in your region")
|
||||
|| body_lower.contains("tiktok is not available")
|
||||
{
|
||||
return "No";
|
||||
}
|
||||
|
||||
"Yes"
|
||||
}
|
||||
|
||||
fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
static REGION_REGEX: OnceLock<Option<Regex>> = OnceLock::new();
|
||||
let regex = REGION_REGEX
|
||||
.get_or_init(|| Regex::new(r#""region"\s*:\s*"([a-zA-Z-]+)""#).ok())
|
||||
.as_ref()?;
|
||||
|
||||
if let Some(caps) = regex.captures(body)
|
||||
&& let Some(matched) = caps.get(1)
|
||||
{
|
||||
let raw = matched.as_str();
|
||||
let country_code = raw.split('-').next().unwrap_or(raw).to_uppercase();
|
||||
if !country_code.is_empty() {
|
||||
let emoji = country_code_to_emoji(&country_code);
|
||||
return Some(format!("{emoji}{country_code}"));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnlockItem {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub region: Option<String>,
|
||||
pub check_time: Option<String>,
|
||||
}
|
||||
|
||||
impl UnlockItem {
|
||||
pub fn pending(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
status: "Pending".to_string(),
|
||||
region: None,
|
||||
check_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 13] = [
|
||||
"哔哩哔哩大陆",
|
||||
"哔哩哔哩港澳台",
|
||||
"ChatGPT iOS",
|
||||
"ChatGPT Web",
|
||||
"Claude",
|
||||
"Gemini",
|
||||
"Youtube Premium",
|
||||
"Bahamut Anime",
|
||||
"Netflix",
|
||||
"Disney+",
|
||||
"Prime Video",
|
||||
"Spotify",
|
||||
"TikTok",
|
||||
];
|
||||
|
||||
pub fn default_unlock_items() -> Vec<UnlockItem> {
|
||||
DEFAULT_UNLOCK_ITEM_NAMES
|
||||
.iter()
|
||||
.map(|name| UnlockItem::pending(name))
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use chrono::Local;
|
||||
|
||||
pub fn get_local_date_string() -> String {
|
||||
let now = Local::now();
|
||||
now.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
pub fn country_code_to_emoji(country_code: &str) -> String {
|
||||
let country_code = country_code.to_uppercase();
|
||||
if country_code.len() < 2 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let bytes = country_code.as_bytes();
|
||||
let c1 = 0x1F1E6 + (bytes[0] as u32) - ('A' as u32);
|
||||
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
||||
|
||||
char::from_u32(c1)
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.youtube.com/premium";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
let mut status = "Failed";
|
||||
let mut region = None;
|
||||
|
||||
if body_lower.contains("youtube premium is not available in your country") {
|
||||
status = "No";
|
||||
} else if body_lower.contains("ad-free") {
|
||||
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => {
|
||||
if let Some(caps) = re.captures(&body)
|
||||
&& let Some(m) = caps.get(1)
|
||||
{
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
region = Some(format!("{emoji}{country_code}"));
|
||||
status = "Yes";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile YouTube Premium regex: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
|
||||
pub type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
// Command modules
|
||||
pub mod app;
|
||||
pub mod backup;
|
||||
pub mod clash;
|
||||
pub mod lightweight;
|
||||
pub mod media_unlock_checker;
|
||||
@@ -21,6 +23,7 @@ pub mod webdav;
|
||||
|
||||
// Re-export all command functions for backwards compatibility
|
||||
pub use app::*;
|
||||
pub use backup::*;
|
||||
pub use clash::*;
|
||||
pub use lightweight::*;
|
||||
pub use media_unlock_checker::*;
|
||||
@@ -35,3 +38,27 @@ pub use uwp::*;
|
||||
pub use validate::*;
|
||||
pub use verge::*;
|
||||
pub use webdav::*;
|
||||
|
||||
pub trait StringifyErr<T> {
|
||||
fn stringify_err(self) -> CmdResult<T>;
|
||||
fn stringify_err_log<F>(self, log_fn: F) -> CmdResult<T>
|
||||
where
|
||||
F: Fn(&str);
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> StringifyErr<T> for Result<T, E> {
|
||||
fn stringify_err(self) -> CmdResult<T> {
|
||||
self.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
fn stringify_err_log<F>(self, log_fn: F) -> CmdResult<T>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let msg = String::from(e.to_string());
|
||||
log_fn(&msg);
|
||||
msg
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use super::CmdResult;
|
||||
use crate::cmd::StringifyErr as _;
|
||||
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::wrap_err;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml_ng::Mapping;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
log::debug!(target: "app", "异步获取系统代理配置");
|
||||
logging!(debug, Type::Network, "异步获取系统代理配置");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
@@ -20,14 +21,21 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回系统代理配置: enable={}, {}:{}",
|
||||
current.enable,
|
||||
current.host,
|
||||
current.port
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
@@ -41,7 +49,13 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
|
||||
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回自动代理配置(缓存): enable={}, url={}",
|
||||
current.enable,
|
||||
current.url
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
@@ -79,10 +93,10 @@ pub fn get_network_interfaces() -> Vec<String> {
|
||||
/// 获取网络接口详细信息
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
|
||||
|
||||
let names = get_network_interfaces();
|
||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||
let interfaces = NetworkInterface::show().stringify_err()?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::CmdResult;
|
||||
use super::StringifyErr as _;
|
||||
use crate::{
|
||||
config::{
|
||||
Config, IProfiles, PrfItem, PrfOption,
|
||||
@@ -10,72 +11,24 @@ use crate::{
|
||||
},
|
||||
core::{CoreManager, handle, timer::Timer, tray::Tray},
|
||||
feat, logging,
|
||||
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||
process::AsyncHandler,
|
||||
ret_err,
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
// 全局请求序列号跟踪,用于避免队列化执行
|
||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
// 策略1: 尝试快速获取latest数据
|
||||
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
|
||||
let profiles = Config::profiles().await;
|
||||
let latest = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match latest_result {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, "快速获取配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2: 如果快速获取失败,尝试获取data()
|
||||
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
|
||||
let profiles = Config::profiles().await;
|
||||
let data = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match data_result {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, "获取draft配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Err(join_err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"获取draft配置任务失败或超时: {}",
|
||||
join_err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3: fallback,尝试重新创建配置
|
||||
logging!(warn, Type::Cmd, "所有获取配置策略都失败,尝试fallback");
|
||||
|
||||
Ok(IProfiles::new().await)
|
||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
||||
let draft = Config::profiles().await;
|
||||
let data = (**draft.data_arc()).clone();
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
@@ -84,8 +37,8 @@ pub async fn enhance_profiles() -> CmdResult {
|
||||
match feat::enhance_profiles().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
return Err(e.to_string());
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
}
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -94,96 +47,67 @@ pub async fn enhance_profiles() -> CmdResult {
|
||||
|
||||
/// 导入配置文件
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
|
||||
|
||||
let import_result = tokio::time::timeout(Duration::from_secs(60), async {
|
||||
let item = PrfItem::from_url(&url, None, None, option).await?;
|
||||
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
|
||||
|
||||
let profiles = Config::profiles().await;
|
||||
let pre_count = profiles
|
||||
.latest_ref()
|
||||
.items
|
||||
.as_ref()
|
||||
.map_or(0, |items| items.len());
|
||||
|
||||
let result = profiles_append_item_safe(item.clone()).await;
|
||||
result?;
|
||||
|
||||
let post_count = profiles
|
||||
.latest_ref()
|
||||
.items
|
||||
.as_ref()
|
||||
.map_or(0, |items| items.len());
|
||||
if post_count <= pre_count {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 配置未增加,导入可能失败");
|
||||
return Err(anyhow::anyhow!("配置导入后数量未增加"));
|
||||
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
|
||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||
Ok(it) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
|
||||
it
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"[导入订阅] 配置保存成功,数量: {} -> {}",
|
||||
pre_count,
|
||||
post_count
|
||||
);
|
||||
|
||||
// 立即发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 下载失败: {}", e);
|
||||
return Err(format!("导入订阅失败: {}", e).into());
|
||||
}
|
||||
};
|
||||
|
||||
// 异步保存配置文件并发送全局通知
|
||||
let uid_clone = item.uid.clone();
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
// 使用Send-safe helper函数
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 保存配置文件失败: {}", e);
|
||||
} else {
|
||||
match profiles_append_item_safe(item).await {
|
||||
Ok(_) => match profiles_save_file_safe().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功");
|
||||
|
||||
// 发送全局配置更新通知
|
||||
if let Some(uid) = uid_clone {
|
||||
// 延迟发送,确保文件已完全写入
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match import_result {
|
||||
Ok(Ok(())) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 导入失败: {}", e);
|
||||
Err(format!("导入订阅失败: {e}"))
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 导入超时(60秒): {}", url);
|
||||
Err("导入订阅超时,请检查网络连接".into())
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 保存配置文件失败: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 保存配置失败: {}", e);
|
||||
return Err(format!("导入订阅失败: {}", e).into());
|
||||
}
|
||||
}
|
||||
// 立即发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
|
||||
// 异步保存配置文件并发送全局通知
|
||||
let uid_clone = item.uid.clone();
|
||||
if let Some(uid) = uid_clone {
|
||||
// 延迟发送,确保文件已完全写入
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
|
||||
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
|
||||
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 调整profile的顺序
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
match profiles_reorder_safe(active_id, over_id).await {
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "重新排序配置文件");
|
||||
logging!(info, Type::Cmd, "重新排序配置文件");
|
||||
Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "重新排序配置文件失败: {}", err);
|
||||
Err(format!("重新排序配置文件失败: {}", err))
|
||||
Config::profiles().await.discard();
|
||||
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
|
||||
Err(format!("重新排序配置文件失败: {}", err).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,30 +116,39 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
/// 创建一个新的配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
match profiles_append_item_with_filedata_safe(item.clone(), file_data).await {
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
// 发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
Config::profiles().await.apply();
|
||||
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => match err.to_string().as_str() {
|
||||
"the file already exists" => Err("the file already exists".into()),
|
||||
_ => Err(format!("add profile error: {err}")),
|
||||
},
|
||||
Err(err) => {
|
||||
Config::profiles().await.discard();
|
||||
match err.to_string().as_str() {
|
||||
"the file already exists" => Err("the file already exists".into()),
|
||||
_ => Err(format!("add profile error: {err}").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新配置文件
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
match feat::update_profile(index, option, Some(true)).await {
|
||||
Ok(_) => Ok(()),
|
||||
match feat::update_profile(&index, option.as_ref(), true, true).await {
|
||||
Ok(_) => {
|
||||
let _: () = Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
Err(e.to_string())
|
||||
Config::profiles().await.discard();
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,366 +157,264 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
// 使用Send-safe helper函数
|
||||
let should_update = wrap_err!(profiles_delete_item_safe(index.clone()).await)?;
|
||||
|
||||
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
|
||||
profiles_save_file_safe().await.stringify_err()?;
|
||||
if should_update {
|
||||
Config::profiles().await.apply();
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
// 发送配置变更通知
|
||||
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
|
||||
handle::Handle::notify_profile_changed(index);
|
||||
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
return Err(e.to_string());
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(false);
|
||||
}
|
||||
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
|
||||
/// 验证新配置文件的语法
|
||||
async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
// 为当前请求分配序列号
|
||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let target_profile = profiles.current.clone();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||
current_sequence,
|
||||
target_profile
|
||||
);
|
||||
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().await.latest_ref().current.clone();
|
||||
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
if let Some(new_profile) = profiles.current.as_ref()
|
||||
&& current_profile.as_ref() != Some(new_profile)
|
||||
{
|
||||
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_data = profiles_config.latest_ref();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_data = profiles_config.latest_arc();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str()));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
let file_read_result = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::fs::read_to_string(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
match yaml_parse_result {
|
||||
Ok(Ok(_)) => {
|
||||
logging!(info, Type::Cmd, "目标配置文件语法正确");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_timeout",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
let file_read_result = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::fs::read_to_string(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
match yaml_parse_result {
|
||||
Ok(Ok(_)) => {
|
||||
logging!(info, Type::Cmd, "目标配置文件语法正确");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::file_read_error", error_msg);
|
||||
Err(())
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行配置更新并处理结果
|
||||
async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile.to_owned()),
|
||||
items: None,
|
||||
};
|
||||
Config::profiles()
|
||||
.await
|
||||
.edit_draft(|d| d.patch_config(&restore_profiles));
|
||||
Config::profiles().await.apply();
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
logging!(info, Type::Cmd, "成功恢复到之前的配置");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
// 检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"正在更新配置草稿,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
|
||||
|
||||
// 在调用内核前再次验证请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
return Ok(false);
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
// 为配置更新添加超时保护
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
if let Some(current) = current_value {
|
||||
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
|
||||
handle::Handle::notify_profile_changed(current.to_owned());
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn handle_validation_failure(
|
||||
error_msg: String,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
if let Some(prev_profile) = current_profile {
|
||||
restore_previous_profile(prev_profile).await?;
|
||||
}
|
||||
handle::Handle::notice_message("config_validate::error", error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
||||
Config::profiles().await.discard();
|
||||
if let Some(prev_profile) = current_profile {
|
||||
restore_previous_profile(prev_profile).await?;
|
||||
}
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn perform_config_update(
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30), // 30秒超时
|
||||
Duration::from_secs(30),
|
||||
CoreManager::global().update_config(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 更新配置并进行验证
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => {
|
||||
// 内核操作完成后再次检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
// crate::process::AsyncHandler::spawn(|| async move {
|
||||
// if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
// log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||
// }
|
||||
// });
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().await.apply();
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Cmd, "成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时处理
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().await.discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
||||
prev_profile,
|
||||
current_sequence
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().await.apply();
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Ok((true, _))) => handle_success(current_value).await,
|
||||
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
|
||||
Ok(Err(e)) => handle_update_error(e).await,
|
||||
Err(_) => handle_timeout(current_profile).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
if CURRENT_SWITCHING_PROFILE
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let target_profile = profiles.current.as_ref();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始修改配置文件,目标profile: {:?}",
|
||||
target_profile
|
||||
);
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
||||
logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
if let Some(switch_to_profile) = target_profile
|
||||
&& previous_profile.as_ref() != Some(switch_to_profile)
|
||||
&& validate_new_profile(switch_to_profile).await.is_err()
|
||||
{
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
return Ok(false);
|
||||
}
|
||||
Config::profiles()
|
||||
.await
|
||||
.edit_draft(|d| d.patch_config(&profiles));
|
||||
|
||||
perform_config_update(target_profile, previous_profile.as_ref()).await
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
|
||||
@@ -601,31 +432,39 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
// 保存修改前检查是否有更新 update_interval
|
||||
let profiles = Config::profiles().await;
|
||||
let update_interval_changed = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) {
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||
&& let Some(new_option) = profile.option.as_ref()
|
||||
{
|
||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
old_interval != new_interval
|
||||
let new_interval = new_option.update_interval;
|
||||
let old_allow_auto_update = old_profile
|
||||
.option
|
||||
.as_ref()
|
||||
.and_then(|o| o.allow_auto_update);
|
||||
let new_allow_auto_update = new_option.allow_auto_update;
|
||||
(old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 保存修改
|
||||
wrap_err!(profiles_patch_item_safe(index.clone(), profile).await)?;
|
||||
profiles_patch_item_safe(&index, &profile)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
|
||||
// 如果更新间隔变更,异步刷新定时器
|
||||
if update_interval_changed {
|
||||
let index_clone = index.clone();
|
||||
// 如果更新间隔或允许自动更新变更,异步刷新定时器
|
||||
if should_refresh_timer {
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
||||
if let Err(e) = crate::core::Timer::global().refresh().await {
|
||||
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
||||
} else {
|
||||
// 刷新成功后发送自定义事件,不触发配置重载
|
||||
crate::core::handle::Handle::notify_timer_updated(index_clone);
|
||||
crate::core::handle::Handle::notify_timer_updated(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -633,29 +472,36 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub async fn view_profile(index: String) -> CmdResult {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
let file = {
|
||||
wrap_err!(profiles_ref.get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
let file = profiles_ref
|
||||
.get_item(&index)
|
||||
.stringify_err()?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")?;
|
||||
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
let path = dirs::app_profiles_dir()
|
||||
.stringify_err()?
|
||||
.join(file.as_str());
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(path))
|
||||
help::open_file(path).stringify_err()
|
||||
}
|
||||
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles_ref.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
PrfItem {
|
||||
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let data = item.read_file().await.stringify_err()?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}");
|
||||
Err(e.to_string())
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,60 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::CoreManager, log_err, wrap_err};
|
||||
use anyhow::Context;
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
config::{Config, ConfigType},
|
||||
core::CoreManager,
|
||||
log_err,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().await.latest_ref().config.clone())
|
||||
Ok(Config::runtime().await.latest_arc().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_ref();
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(
|
||||
config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(|config| serde_yaml_ng::to_string(config)
|
||||
.context("failed to convert config to yaml"))
|
||||
)
|
||||
config
|
||||
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(|config| {
|
||||
serde_yaml_ng::to_string(config)
|
||||
.context("failed to convert config to yaml")
|
||||
.map(|s| s.into())
|
||||
})
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().await.latest_ref().exists_keys.clone())
|
||||
Ok(Config::runtime().await.latest_arc().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
|
||||
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_ref();
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
let config = wrap_err!(
|
||||
runtime
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
)?;
|
||||
let config = runtime
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
||||
.stringify_err()?;
|
||||
|
||||
if let Some(serde_yaml_ng::Value::Sequence(proxies)) = config.get("proxies") {
|
||||
let mut proxy_name = Some(Some(proxy_chain_exit_node.as_str()));
|
||||
@@ -78,13 +85,14 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
||||
|
||||
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
|
||||
|
||||
config.insert("proxies".to_string(), proxies_chain);
|
||||
config.insert("proxies".into(), proxies_chain);
|
||||
|
||||
wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed"))
|
||||
serde_yaml_ng::to_string(&config)
|
||||
.context("YAML generation failed")
|
||||
.map(|s| s.into())
|
||||
.stringify_err()
|
||||
} else {
|
||||
wrap_err!(Err(anyhow::anyhow!(
|
||||
"failed to get proxies or proxy-groups".to_string()
|
||||
)))
|
||||
Err("failed to get proxies or proxy-groups".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,14 +103,14 @@ pub async fn update_proxy_chain_config_in_runtime(
|
||||
) -> CmdResult<()> {
|
||||
{
|
||||
let runtime = Config::runtime().await;
|
||||
let mut draft = runtime.draft_mut();
|
||||
draft.update_proxy_chain_config(proxy_chain_config);
|
||||
drop(draft);
|
||||
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
|
||||
runtime.apply();
|
||||
}
|
||||
|
||||
// 生成新的运行配置文件并通知 Clash 核心重新加载
|
||||
let run_path = wrap_err!(Config::generate_file(ConfigType::Run).await)?;
|
||||
let run_path = Config::generate_file(ConfigType::Run)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
log_err!(CoreManager::global().put_configs_force(run_path).await);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
cmd::StringifyErr as _,
|
||||
config::{Config, PrfItem},
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
logging,
|
||||
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_guard = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let content = wrap_err!(item.read_file())?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||
(profiles_dir.join(path), content, is_merge)
|
||||
let file_data = match file_data {
|
||||
Some(d) => d,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
let file_data = file_data.ok_or("file_data is None")?;
|
||||
wrap_err!(fs::write(&file_path, &file_data).await)?;
|
||||
let backup_trigger = match index.as_str() {
|
||||
"Merge" => Some(AutoBackupTrigger::GlobalMerge),
|
||||
"Script" => Some(AutoBackupTrigger::GlobalScript),
|
||||
_ => Some(AutoBackupTrigger::ProfileChange),
|
||||
};
|
||||
|
||||
// 在异步操作前获取必要元数据并释放锁
|
||||
let (rel_path, is_merge_file) = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_guard = profiles.latest_arc();
|
||||
let item = profiles_guard.get_item(&index).stringify_err()?;
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
(path, is_merge)
|
||||
};
|
||||
|
||||
// 读取原始内容(在释放profiles_guard后进行)
|
||||
let original_content = PrfItem {
|
||||
file: Some(rel_path.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.read_file()
|
||||
.await
|
||||
.stringify_err()?;
|
||||
|
||||
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
|
||||
let file_path = profiles_dir.join(rel_path.as_str());
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
|
||||
// 保存新的配置文件
|
||||
fs::write(&file_path, &file_data).await.stringify_err()?;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
@@ -41,117 +58,127 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, Some(true))
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
|
||||
// 成功后尝试更新整体配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
// 配置更新成功,刷新前端
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
let changes_applied = if is_merge_file {
|
||||
handle_merge_file(&file_path_str, &file_path, &original_content).await?
|
||||
} else {
|
||||
handle_full_validation(&file_path_str, &file_path, &original_content).await?
|
||||
};
|
||||
|
||||
if changes_applied && let Some(trigger) = backup_trigger {
|
||||
AutoBackupManager::trigger_backup(trigger);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn restore_original(
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> Result<(), String> {
|
||||
fs::write(file_path, original_content).await.stringify_err()
|
||||
}
|
||||
|
||||
fn is_script_error(err: &str, file_path_str: &str) -> bool {
|
||||
file_path_str.ends_with(".js")
|
||||
|| err.contains("Script syntax error")
|
||||
|| err.contains("Script must contain a main function")
|
||||
|| err.contains("Failed to read script file")
|
||||
}
|
||||
|
||||
async fn handle_merge_file(
|
||||
file_path_str: &str,
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
|
||||
match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||
return Err(e.to_string());
|
||||
} else {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
restore_original(file_path, original_content).await?;
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
restore_original(file_path, original_content).await?;
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非merge文件使用完整验证流程
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, None)
|
||||
.await
|
||||
{
|
||||
async fn handle_full_validation(
|
||||
file_path_str: &str,
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> CmdResult<bool> {
|
||||
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||
|
||||
// 智能判断错误类型
|
||||
let is_script_error = file_path_str.ends_with(".js")
|
||||
|| error_msg.contains("Script syntax error")
|
||||
|| error_msg.contains("Script must contain a main function")
|
||||
|| error_msg.contains("Failed to read script file");
|
||||
restore_original(file_path, original_content).await?;
|
||||
|
||||
if error_msg.contains("YAML syntax error")
|
||||
|| error_msg.contains("Failed to read file:")
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str))
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] YAML配置文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.clone());
|
||||
let result = (false, error_msg.to_owned());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
} else if is_script_error(&error_msg, file_path_str) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 脚本文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.clone());
|
||||
let result = (false, error_msg.to_owned());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 其他类型验证失败,发送一般通知"
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
handle::Handle::notice_message("config_validate::error", error_msg.to_owned());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||
Err(e.to_string())
|
||||
restore_original(file_path, original_content).await?;
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||
utils::i18n::t,
|
||||
};
|
||||
use super::{CmdResult, StringifyErr as _};
|
||||
use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus};
|
||||
use smartstring::SmartString;
|
||||
|
||||
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
||||
if let Err(e) = SERVICE_MANAGER
|
||||
@@ -12,7 +10,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
|
||||
.await
|
||||
{
|
||||
let emsg = format!("{} Service failed: {}", op_type, e);
|
||||
return Err(t(emsg.as_str()).await);
|
||||
return Err(SmartString::from(emsg));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -39,8 +37,6 @@ pub async fn repair_service() -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_service_available() -> CmdResult<bool> {
|
||||
service::is_service_available()
|
||||
.await
|
||||
.map(|_| true)
|
||||
.map_err(|e| e.to_string())
|
||||
service::is_service_available().await.stringify_err()?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{CoreManager, handle},
|
||||
core::{CoreManager, handle, manager::RunningMode},
|
||||
logging,
|
||||
module::sysinfo::PlatformSpecification,
|
||||
utils::logging::Type,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
sync::atomic::{AtomicI64, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt as _;
|
||||
use tokio::time::Instant;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
// 获取当前系统时间,转换为毫秒级时间戳
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
AtomicI64::new(now)
|
||||
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
|
||||
#[cfg(target_os = "windows")]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
@@ -45,52 +47,18 @@ pub async fn get_system_info() -> CmdResult<String> {
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
Ok(CoreManager::global().get_running_mode().to_string())
|
||||
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
|
||||
Ok(CoreManager::global().get_running_mode())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
Ok(now - start_time)
|
||||
pub fn get_app_uptime() -> CmdResult<u128> {
|
||||
Ok(APP_START_TIME.elapsed().as_millis())
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
|
||||
let result = Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 非Windows平台检测是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
Ok(*APPS_RUN_AS_ADMIN)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use super::CmdResult;
|
||||
use crate::cmd::CmdResult;
|
||||
|
||||
/// Platform-specific implementation for UWP functionality
|
||||
#[cfg(windows)]
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
use crate::{core::win_uwp, wrap_err};
|
||||
use crate::cmd::CmdResult;
|
||||
use crate::cmd::StringifyErr as _;
|
||||
use crate::core::win_uwp;
|
||||
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools())
|
||||
win_uwp::invoke_uwptools().stringify_err()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +17,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
pub const fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use super::CmdResult;
|
||||
use crate::{core::*, logging, utils::logging::Type};
|
||||
use crate::{
|
||||
core::{handle, validate::CoreConfigValidator},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
#[tauri::command]
|
||||
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||
handle::Handle::notice_message(&status, &msg);
|
||||
handle::Handle::notice_message(status.as_str(), msg.as_str());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -29,7 +34,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
};
|
||||
|
||||
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +43,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
logging!(info, Type::Config, "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
.await
|
||||
{
|
||||
match CoreConfigValidator::validate_config_file(&file_path, None).await {
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
@@ -116,6 +118,6 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
status,
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, feat, wrap_err};
|
||||
use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox};
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge().await;
|
||||
let verge_data = {
|
||||
let ref_data = verge.latest_ref();
|
||||
ref_data.clone()
|
||||
};
|
||||
let verge_response = IVergeResponse::from(*verge_data);
|
||||
Ok(verge_response)
|
||||
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
|
||||
feat::fetch_verge_config().await.stringify_err()
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload, false).await)
|
||||
feat::patch_verge(&payload, false).await.stringify_err()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core, feat, wrap_err};
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
config::{Config, IVerge},
|
||||
core, feat,
|
||||
};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// 保存 WebDAV 配置
|
||||
#[tauri::command]
|
||||
@@ -11,18 +16,11 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(patch.clone());
|
||||
Config::verge().await.edit_draft(|e| e.patch_config(&patch));
|
||||
Config::verge().await.apply();
|
||||
|
||||
// 分离数据获取和异步调用
|
||||
let verge_data = Config::verge().await.latest_ref().clone();
|
||||
verge_data
|
||||
.save_file()
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
let verge_data = Config::verge().await.data_arc();
|
||||
verge_data.save_file().await.stringify_err()?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
@@ -30,23 +28,25 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
/// 创建 WebDAV 备份并上传
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
feat::create_backup_and_upload_webdav()
|
||||
.await
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
/// 列出 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
feat::list_wevdav_backup().await.stringify_err()
|
||||
}
|
||||
|
||||
/// 删除 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
feat::delete_webdav_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
/// 从 WebDAV 恢复备份文件
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
feat::restore_webdav_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::config::Config;
|
||||
use crate::constants::{network, tun as tun_const};
|
||||
use crate::utils::dirs::{ipc_path, path_to_str};
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
str::FromStr as _,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@@ -35,12 +37,12 @@ impl IClashTemp {
|
||||
if let Some(Value::String(s)) = map.get_mut("secret")
|
||||
&& s.is_empty()
|
||||
{
|
||||
*s = "set-your-secret".to_string();
|
||||
*s = "set-your-secret".into();
|
||||
}
|
||||
Self(Self::guard(map))
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
template
|
||||
}
|
||||
}
|
||||
@@ -48,29 +50,32 @@ impl IClashTemp {
|
||||
|
||||
pub fn template() -> Self {
|
||||
let mut map = Mapping::new();
|
||||
let mut tun = Mapping::new();
|
||||
let mut tun_config = Mapping::new();
|
||||
let mut cors_map = Mapping::new();
|
||||
tun.insert("enable".into(), false.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
tun.insert("stack".into(), "mixed".into());
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
tun.insert("stack".into(), "gvisor".into());
|
||||
tun.insert("auto-route".into(), true.into());
|
||||
tun.insert("strict-route".into(), false.into());
|
||||
tun.insert("auto-detect-interface".into(), true.into());
|
||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||
|
||||
tun_config.insert("enable".into(), false.into());
|
||||
tun_config.insert("stack".into(), tun_const::DEFAULT_STACK.into());
|
||||
tun_config.insert("auto-route".into(), true.into());
|
||||
tun_config.insert("strict-route".into(), false.into());
|
||||
tun_config.insert("auto-detect-interface".into(), true.into());
|
||||
tun_config.insert("dns-hijack".into(), tun_const::DNS_HIJACK.into());
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
map.insert("redir-port".into(), 7895.into());
|
||||
map.insert("redir-port".into(), network::ports::DEFAULT_REDIR.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
map.insert("tproxy-port".into(), 7896.into());
|
||||
map.insert("mixed-port".into(), 7897.into());
|
||||
map.insert("socks-port".into(), 7898.into());
|
||||
map.insert("port".into(), 7899.into());
|
||||
map.insert("log-level".into(), "warning".into());
|
||||
map.insert("tproxy-port".into(), network::ports::DEFAULT_TPROXY.into());
|
||||
|
||||
map.insert("mixed-port".into(), network::ports::DEFAULT_MIXED.into());
|
||||
map.insert("socks-port".into(), network::ports::DEFAULT_SOCKS.into());
|
||||
map.insert("port".into(), network::ports::DEFAULT_HTTP.into());
|
||||
map.insert("log-level".into(), "info".into());
|
||||
map.insert("allow-lan".into(), false.into());
|
||||
map.insert("ipv6".into(), true.into());
|
||||
map.insert("mode".into(), "rule".into());
|
||||
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||
map.insert(
|
||||
"external-controller".into(),
|
||||
network::DEFAULT_EXTERNAL_CONTROLLER.into(),
|
||||
);
|
||||
#[cfg(unix)]
|
||||
map.insert(
|
||||
"external-controller-unix".into(),
|
||||
@@ -81,6 +86,7 @@ impl IClashTemp {
|
||||
"external-controller-pipe".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
);
|
||||
map.insert("tun".into(), tun_config.into());
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
cors_map.insert(
|
||||
"allow-origins".into(),
|
||||
@@ -97,7 +103,6 @@ impl IClashTemp {
|
||||
.into(),
|
||||
);
|
||||
map.insert("secret".into(), "set-your-secret".into());
|
||||
map.insert("tun".into(), tun.into());
|
||||
map.insert("external-controller-cors".into(), cors_map.into());
|
||||
map.insert("unified-delay".into(), true.into());
|
||||
Self(map)
|
||||
@@ -209,9 +214,9 @@ impl IClashTemp {
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7896);
|
||||
.unwrap_or(network::ports::DEFAULT_TPROXY);
|
||||
if port == 0 {
|
||||
port = 7896;
|
||||
port = network::ports::DEFAULT_TPROXY;
|
||||
}
|
||||
port
|
||||
}
|
||||
@@ -282,7 +287,7 @@ impl IClashTemp {
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
.unwrap_or("127.0.0.1:9097".into())
|
||||
.unwrap_or_else(|| "127.0.0.1:9097".into())
|
||||
}
|
||||
|
||||
pub fn guard_external_controller(config: &Mapping) -> String {
|
||||
@@ -295,7 +300,7 @@ impl IClashTemp {
|
||||
// 检查 enable_external_controller 设置,用于运行时配置生成
|
||||
let enable_external_controller = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.latest_arc()
|
||||
.enable_external_controller
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -323,10 +328,10 @@ impl IClashTemp {
|
||||
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
|
||||
ipc_path()
|
||||
.ok()
|
||||
.and_then(|path| path_to_str(&path).ok().map(|s| s.to_string()))
|
||||
.and_then(|path| path_to_str(&path).ok().map(|s| s.into()))
|
||||
.unwrap_or_else(|| {
|
||||
log::error!(target: "app", "Failed to get IPC path, using default");
|
||||
"127.0.0.1:9090".to_string()
|
||||
logging!(error, Type::Config, "Failed to get IPC path");
|
||||
crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,102 @@
|
||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{PrfItem, profiles_append_item_safe},
|
||||
core::{CoreManager, handle},
|
||||
enhance, logging,
|
||||
utils::{dirs, help, logging::Type},
|
||||
constants::{files, timing},
|
||||
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
||||
enhance, logging, logging_error,
|
||||
utils::{Draft, dirs, help, logging::Type},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||
use smartstring::alias::String;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<Box<IClashTemp>>,
|
||||
verge_config: Draft<Box<IVerge>>,
|
||||
profiles_config: Draft<Box<IProfiles>>,
|
||||
runtime_config: Draft<Box<IRuntime>>,
|
||||
clash_config: Draft<IClashTemp>,
|
||||
verge_config: Draft<IVerge>,
|
||||
profiles_config: Draft<IProfiles>,
|
||||
runtime_config: Draft<IRuntime>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn global() -> &'static Config {
|
||||
pub async fn global() -> &'static Self {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::const_new();
|
||||
CONFIG
|
||||
.get_or_init(|| async {
|
||||
Config {
|
||||
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
|
||||
verge_config: Draft::from(Box::new(IVerge::new().await)),
|
||||
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
|
||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||
Self {
|
||||
clash_config: Draft::new(IClashTemp::new().await),
|
||||
verge_config: Draft::new(IVerge::new().await),
|
||||
profiles_config: Draft::new(IProfiles::new().await),
|
||||
runtime_config: Draft::new(IRuntime::new()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn clash() -> Draft<Box<IClashTemp>> {
|
||||
pub async fn clash() -> Draft<IClashTemp> {
|
||||
Self::global().await.clash_config.clone()
|
||||
}
|
||||
|
||||
pub async fn verge() -> Draft<Box<IVerge>> {
|
||||
pub async fn verge() -> Draft<IVerge> {
|
||||
Self::global().await.verge_config.clone()
|
||||
}
|
||||
|
||||
pub async fn profiles() -> Draft<Box<IProfiles>> {
|
||||
pub async fn profiles() -> Draft<IProfiles> {
|
||||
Self::global().await.profiles_config.clone()
|
||||
}
|
||||
|
||||
pub async fn runtime() -> Draft<Box<IRuntime>> {
|
||||
pub async fn runtime() -> Draft<IRuntime> {
|
||||
Self::global().await.runtime_config.clone()
|
||||
}
|
||||
|
||||
/// 初始化订阅
|
||||
pub async fn init_config() -> Result<()> {
|
||||
if Self::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.get_item(&"Merge".to_string())
|
||||
.is_err()
|
||||
Self::ensure_default_profile_items().await?;
|
||||
|
||||
// init Tun mode
|
||||
if !cmd::system::is_admin().unwrap_or_default()
|
||||
&& service::is_service_available().await.is_err()
|
||||
{
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||
profiles_append_item_safe(merge_item.clone()).await?;
|
||||
let verge = Self::verge().await;
|
||||
verge.edit_draft(|d| {
|
||||
d.enable_tun_mode = Some(false);
|
||||
});
|
||||
verge.apply();
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
|
||||
// 分离数据获取和异步调用避免Send问题
|
||||
let verge_data = Self::verge().await.latest_arc();
|
||||
logging_error!(Type::Core, verge_data.save_file().await);
|
||||
}
|
||||
if Self::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.get_item(&"Script".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||
profiles_append_item_safe(script_item.clone()).await?;
|
||||
|
||||
let validation_result = Self::generate_and_validate().await?;
|
||||
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
|
||||
async fn ensure_default_profile_items() -> Result<()> {
|
||||
let profiles = Self::profiles().await;
|
||||
if profiles.latest_arc().get_item("Merge").is_err() {
|
||||
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
|
||||
profiles_append_item_safe(merge_item).await?;
|
||||
}
|
||||
if profiles.latest_arc().get_item("Script").is_err() {
|
||||
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
|
||||
profiles_append_item_safe(script_item).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_and_validate() -> Result<Option<(&'static str, String)>> {
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, "生成运行时配置失败: {}", err);
|
||||
@@ -83,11 +107,11 @@ impl Config {
|
||||
// 生成运行时配置文件并验证
|
||||
let config_result = Self::generate_file(ConfigType::Run).await;
|
||||
|
||||
let validation_result = if config_result.is_ok() {
|
||||
if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
logging!(info, Type::Config, "开始验证配置");
|
||||
|
||||
match CoreManager::global().validate_config().await {
|
||||
match CoreConfigValidator::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
if !is_valid {
|
||||
logging!(
|
||||
@@ -99,12 +123,12 @@ impl Config {
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
Ok(Some(("config_validate::boot_error", error_msg)))
|
||||
} else {
|
||||
logging!(info, Type::Config, "配置验证成功");
|
||||
// 前端没有必要知道验证成功的消息,也没有事件驱动
|
||||
// Some(("config_validate::success", String::new()))
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -112,7 +136,7 @@ impl Config {
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
Ok(Some(("config_validate::process_terminated", String::new())))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -120,54 +144,42 @@ impl Config {
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
// 在单独的任务中发送通知
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
handle::Handle::notice_message(msg_type, &msg_content);
|
||||
Ok(Some(("config_validate::error", String::new())))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将订阅丢到对应的文件中
|
||||
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
let path = match typ {
|
||||
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
|
||||
ConfigType::Run => dirs::app_home_dir()?.join(files::RUNTIME_CONFIG),
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
|
||||
};
|
||||
|
||||
let runtime = Config::runtime().await;
|
||||
let config = runtime
|
||||
.latest_ref()
|
||||
let runtime = Self::runtime().await;
|
||||
let runtime_arc = runtime.latest_arc();
|
||||
let config = runtime_arc
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("failed to get runtime config"))?
|
||||
.clone();
|
||||
drop(runtime); // 显式释放锁
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
|
||||
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 生成订阅存好
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
Self::runtime().await.edit_draft(|d| {
|
||||
*d = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify_config_initialization() {
|
||||
logging!(info, Type::Setup, "Verifying config initialization...");
|
||||
|
||||
let backoff_strategy = ExponentialBackoff {
|
||||
initial_interval: std::time::Duration::from_millis(100),
|
||||
max_interval: std::time::Duration::from_secs(2),
|
||||
@@ -177,49 +189,15 @@ impl Config {
|
||||
};
|
||||
|
||||
let operation = || async {
|
||||
if Config::runtime().await.latest_ref().config.is_some() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
"Config initialization verified successfully"
|
||||
);
|
||||
if Self::runtime().await.latest_arc().config.is_some() {
|
||||
return Ok::<(), BackoffError<anyhow::Error>>(());
|
||||
}
|
||||
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
"Runtime config not found, attempting to regenerate..."
|
||||
);
|
||||
|
||||
match Config::generate().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Setup, "Config successfully regenerated");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Setup, "Failed to generate config: {}", e);
|
||||
Err(BackoffError::transient(e))
|
||||
}
|
||||
}
|
||||
Self::generate().await.map_err(BackoffError::transient)
|
||||
};
|
||||
|
||||
match backoff::future::retry(backoff_strategy, operation).await {
|
||||
Ok(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
"Config initialization verified with backoff retry"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
"Failed to verify config initialization after retries: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
|
||||
logging!(error, Type::Setup, "Config init verification failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,8 +216,8 @@ mod tests {
|
||||
#[allow(unused_variables)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_prfitem_from_merge_size() {
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))
|
||||
.expect("Failed to create merge item in test");
|
||||
let merge_item =
|
||||
PrfItem::from_merge(Some("Merge".into())).expect("Failed to create merge item in test");
|
||||
let prfitem_size = mem::size_of_val(&merge_item);
|
||||
// Boxed version
|
||||
let boxed_merge_item = Box::new(merge_item);
|
||||
@@ -252,7 +230,7 @@ mod tests {
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
fn test_draft_size_non_boxed() {
|
||||
let draft = Draft::from(IRuntime::new());
|
||||
let draft = Draft::new(IRuntime::new());
|
||||
let iruntime_size = std::mem::size_of_val(&draft);
|
||||
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
||||
}
|
||||
@@ -260,7 +238,7 @@ mod tests {
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
fn test_draft_size_boxed() {
|
||||
let draft = Draft::from(Box::new(IRuntime::new()));
|
||||
let draft = Draft::new(Box::new(IRuntime::new()));
|
||||
let box_iruntime_size = std::mem::size_of_val(&draft);
|
||||
assert_eq!(
|
||||
box_iruntime_size,
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::{
|
||||
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
|
||||
RwLockUpgradableReadGuard, RwLockWriteGuard,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone + ToOwned> {
|
||||
inner: Arc<RwLock<(T, Option<T>)>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + ToOwned> From<T> for Draft<T> {
|
||||
fn from(data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new((data, None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
|
||||
/// # Type Parameters
|
||||
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
|
||||
///
|
||||
/// # Methods
|
||||
/// - `data_mut`: Returns a mutable reference to the committed data.
|
||||
/// - `data_ref`: Returns an immutable reference to the committed data.
|
||||
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
|
||||
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
|
||||
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
|
||||
/// - `discard`: Discards the draft data and returns it if it existed.
|
||||
impl<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
/// 可写正式数据
|
||||
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
|
||||
}
|
||||
|
||||
/// 返回正式数据的只读视图(不包含草稿)
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 创建或获取草稿并返回可写引用
|
||||
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
let guard = self.inner.upgradable_read();
|
||||
if guard.1.is_none() {
|
||||
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
|
||||
guard.1 = Some(guard.0.clone());
|
||||
return RwLockWriteGuard::map(guard, |inner| {
|
||||
inner.1.as_mut().unwrap_or_else(|| {
|
||||
unreachable!("Draft was just created above, this should never fail")
|
||||
})
|
||||
});
|
||||
}
|
||||
// 已存在草稿,升级为写锁映射
|
||||
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
|
||||
inner
|
||||
.1
|
||||
.as_mut()
|
||||
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
|
||||
})
|
||||
}
|
||||
|
||||
/// 零拷贝只读视图:返回草稿(若存在)或正式值
|
||||
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| {
|
||||
inner.1.as_ref().unwrap_or(&inner.0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 提交草稿,返回旧正式数据
|
||||
pub fn apply(&self) -> Option<Box<T>> {
|
||||
let mut inner = self.inner.write();
|
||||
inner
|
||||
.1
|
||||
.take()
|
||||
.map(|draft| std::mem::replace(&mut inner.0, draft))
|
||||
}
|
||||
|
||||
/// 丢弃草稿,返回被丢弃的草稿
|
||||
pub fn discard(&self) -> Option<Box<T>> {
|
||||
self.inner.write().1.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draft_box() {
|
||||
use super::IVerge;
|
||||
|
||||
// 1. 创建 Draft<Box<IVerge>>
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..IVerge::default()
|
||||
});
|
||||
let draft = Draft::from(verge);
|
||||
|
||||
// 2. 读取正式数据(data_mut)
|
||||
{
|
||||
let data = draft.data_mut();
|
||||
assert_eq!(data.enable_auto_launch, Some(true));
|
||||
assert_eq!(data.enable_tun_mode, Some(false));
|
||||
}
|
||||
|
||||
// 3. 初次获取草稿(draft_mut 会自动 clone 一份)
|
||||
{
|
||||
let draft_view = draft.draft_mut();
|
||||
assert_eq!(draft_view.enable_auto_launch, Some(true));
|
||||
assert_eq!(draft_view.enable_tun_mode, Some(false));
|
||||
}
|
||||
|
||||
// 4. 修改草稿
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
}
|
||||
|
||||
// 正式数据未变
|
||||
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
|
||||
|
||||
// 草稿已变
|
||||
{
|
||||
let latest = draft.latest_ref();
|
||||
assert_eq!(latest.enable_auto_launch, Some(false));
|
||||
assert_eq!(latest.enable_tun_mode, Some(true));
|
||||
}
|
||||
|
||||
// 5. 提交草稿
|
||||
assert!(draft.apply().is_some()); // 第一次提交应有返回
|
||||
assert!(draft.apply().is_none()); // 第二次提交返回 None
|
||||
|
||||
// 正式数据已更新
|
||||
{
|
||||
let data = draft.data_mut();
|
||||
assert_eq!(data.enable_auto_launch, Some(false));
|
||||
assert_eq!(data.enable_tun_mode, Some(true));
|
||||
}
|
||||
|
||||
// 6. 新建并修改下一轮草稿
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(true);
|
||||
}
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
|
||||
|
||||
// 7. 丢弃草稿
|
||||
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
|
||||
assert!(draft.discard().is_none()); // 再次丢弃返回 None
|
||||
|
||||
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
use crate::utils::dirs::get_encryption_key;
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key,
|
||||
aead::{Aead, KeyInit},
|
||||
aead::{Aead as _, KeyInit as _},
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
|
||||
const NONCE_LENGTH: usize = 12;
|
||||
|
||||
// Use task-local context so the flag follows the async task across threads
|
||||
tokio::task_local! {
|
||||
static ENCRYPTION_ACTIVE: Cell<bool>;
|
||||
}
|
||||
|
||||
/// Encrypt data
|
||||
#[allow(deprecated)]
|
||||
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let encryption_key = get_encryption_key()?;
|
||||
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
|
||||
@@ -30,6 +38,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
/// Decrypt data
|
||||
#[allow(deprecated)]
|
||||
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let encryption_key = get_encryption_key()?;
|
||||
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
|
||||
@@ -57,39 +66,45 @@ where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
// 如果序列化失败,返回 None
|
||||
let json = match serde_json::to_string(value) {
|
||||
Ok(j) => j,
|
||||
Err(_) => return serializer.serialize_none(),
|
||||
};
|
||||
|
||||
// 如果加密失败,返回 None
|
||||
match encrypt_data(&json) {
|
||||
Ok(encrypted) => serializer.serialize_str(&encrypted),
|
||||
Err(_) => serializer.serialize_none(),
|
||||
if is_encryption_active() {
|
||||
let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
|
||||
let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
|
||||
serializer.serialize_str(&encrypted)
|
||||
} else {
|
||||
value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize decrypted function
|
||||
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + Default,
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
// 如果反序列化字符串失败,返回默认值
|
||||
let encrypted = match String::deserialize(deserializer) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
if is_encryption_active() {
|
||||
let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
|
||||
|
||||
// 如果解密失败,返回默认值
|
||||
let decrypted_string = match decrypt_data(&encrypted) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
// 如果 JSON 解析失败,返回默认值
|
||||
match serde_json::from_str(&decrypted_string) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(T::default()),
|
||||
match encrypted_opt {
|
||||
Some(encrypted) if !encrypted.is_empty() => {
|
||||
let decrypted_string =
|
||||
decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
|
||||
serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
|
||||
}
|
||||
_ => Ok(T::default()),
|
||||
}
|
||||
} else {
|
||||
T::deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_encryption<F, Fut, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = R>,
|
||||
{
|
||||
ENCRYPTION_ACTIVE.scope(Cell::new(true), f()).await
|
||||
}
|
||||
|
||||
fn is_encryption_active() -> bool {
|
||||
ENCRYPTION_ACTIVE.try_with(|c| c.get()).unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
mod clash;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod config;
|
||||
mod draft;
|
||||
mod encrypt;
|
||||
mod prfitem;
|
||||
pub mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::{
|
||||
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
||||
};
|
||||
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*};
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user