Compare commits
173 Commits
updater-al
...
v0.2.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9a2f221db | ||
|
|
a4b3a257ed | ||
|
|
10397d0847 | ||
|
|
db442b2746 | ||
|
|
8cb3c69b78 | ||
|
|
967f21cc23 | ||
|
|
3ecd73f430 | ||
|
|
ca7f6b86d7 | ||
|
|
00cee81812 | ||
|
|
25f5db82dc | ||
|
|
9e5c5d5e69 | ||
|
|
2cfd1784d8 | ||
|
|
bec1b95ad3 | ||
|
|
e26f500ad0 | ||
|
|
9c33f007a1 | ||
|
|
902256d461 | ||
|
|
6051bd6d06 | ||
|
|
c82f4e50d2 | ||
|
|
94e785c75c | ||
|
|
8b8daa7b4c | ||
|
|
c95e63014f | ||
|
|
32bf42cbb9 | ||
|
|
175ec98947 | ||
|
|
0abd9343a9 | ||
|
|
c9976382a9 | ||
|
|
d38e93ac7e | ||
|
|
e51f1d20c0 | ||
|
|
df595f4835 | ||
|
|
63e4d2f686 | ||
|
|
971580def8 | ||
|
|
ffd32426b5 | ||
|
|
d2d26cc822 | ||
|
|
a373b0b6eb | ||
|
|
f515fa1443 | ||
|
|
e32e83d45e | ||
|
|
7be3cdeb65 | ||
|
|
b234b9166d | ||
|
|
2c485b5efb | ||
|
|
b7d7e1a1af | ||
|
|
01be6ae70a | ||
|
|
445eaadac3 | ||
|
|
d5b1dfddee | ||
|
|
c68ea04f06 | ||
|
|
9abc30b60c | ||
|
|
1f7561298c | ||
|
|
611c5757e0 | ||
|
|
ab56e82173 | ||
|
|
34350fadb6 | ||
|
|
77786da53f | ||
|
|
f794ca5426 | ||
|
|
a2010e6d1d | ||
|
|
4ce6e9bfd7 | ||
|
|
9a3794073b | ||
|
|
d6197d6d21 | ||
|
|
1f321cf6bc | ||
|
|
5c6d3f4078 | ||
|
|
6b8b95e4ca | ||
|
|
ae08d48641 | ||
|
|
d1ce5566cf | ||
|
|
5f027ebc79 | ||
|
|
8cf83f8338 | ||
|
|
b96e2c1fe0 | ||
|
|
4ad1379773 | ||
|
|
ef0883f732 | ||
|
|
a2076b4e2d | ||
|
|
0a3998530e | ||
|
|
ed2ec56a44 | ||
|
|
87473bdf92 | ||
|
|
8186a6841a | ||
|
|
0a0b5b6612 | ||
|
|
72704f9dc9 | ||
|
|
06ad23d904 | ||
|
|
fbd1c55f44 | ||
|
|
9668a04a1a | ||
|
|
24af375a8e | ||
|
|
a32c973ab8 | ||
|
|
50beb913de | ||
|
|
05f1ec7b34 | ||
|
|
9271b107b6 | ||
|
|
e7208dd7d2 | ||
|
|
e5dfb34082 | ||
|
|
2ba5c4e706 | ||
|
|
27bcc5f4f8 | ||
|
|
d884bd539b | ||
|
|
580a56727c | ||
|
|
ac3163d061 | ||
|
|
8bc7a6c3e1 | ||
|
|
31d368979e | ||
|
|
5e855e4755 | ||
|
|
a8b75aeabd | ||
|
|
854d42180a | ||
|
|
e94724595c | ||
|
|
6f1d9ba1b4 | ||
|
|
c090ae3b11 | ||
|
|
3303e95713 | ||
|
|
5cdc5075f8 | ||
|
|
eb1e4fe0c3 | ||
|
|
b1e3283a24 | ||
|
|
ce3b0bb479 | ||
|
|
25b295f2a8 | ||
|
|
18b7366258 | ||
|
|
565771a3ea | ||
|
|
f9376f6903 | ||
|
|
8e0f5b6abd | ||
|
|
41f32231f0 | ||
|
|
e1968891ac | ||
|
|
f04e707b10 | ||
|
|
0bb795b0e1 | ||
|
|
1c5e43690e | ||
|
|
f604416532 | ||
|
|
87ee07d481 | ||
|
|
7dec9cbe9b | ||
|
|
1274ba2324 | ||
|
|
d6014865d6 | ||
|
|
48a5ff6948 | ||
|
|
dd3950e46d | ||
|
|
1708246866 | ||
|
|
b0734f5935 | ||
|
|
11768862d3 | ||
|
|
ef409216d8 | ||
|
|
f739afea3d | ||
|
|
d5266fa003 | ||
|
|
149bdd5175 | ||
|
|
ec99e24ca1 | ||
|
|
7cc893383e | ||
|
|
3902480d39 | ||
|
|
686490ded1 | ||
|
|
4435a5aee4 | ||
|
|
d9e3a47894 | ||
|
|
c96be18187 | ||
|
|
47416dd3f8 | ||
|
|
4486f734bb | ||
|
|
bea6a2c8f7 | ||
|
|
82af9ed78e | ||
|
|
2b9e38d259 | ||
|
|
7c4222aed2 | ||
|
|
a574ced428 | ||
|
|
cf437e6d94 | ||
|
|
e1bb8aa125 | ||
|
|
c11bdd81e9 | ||
|
|
f1192c95a8 | ||
|
|
ae187cc21a | ||
|
|
a7875718f7 | ||
|
|
5db1f7cda7 | ||
|
|
cb98b17052 | ||
|
|
7aee1c6d6e | ||
|
|
f22199b7d9 | ||
|
|
0a8d6e5147 | ||
|
|
6d519dac1e | ||
|
|
d5a174c71b | ||
|
|
628de70e89 | ||
|
|
fee08f3826 | ||
|
|
bdfc383a18 | ||
|
|
f6b5524e0e | ||
|
|
e7461fccab | ||
|
|
a92872c831 | ||
|
|
094feb74ec | ||
|
|
9b1c660306 | ||
|
|
4b860ba897 | ||
|
|
3d8b2cf35f | ||
|
|
41fc13cfe2 | ||
|
|
5fde5dcc7c | ||
|
|
034885d810 | ||
|
|
3f7a7b8cd2 | ||
|
|
98dc50a9ed | ||
|
|
5dd820d12e | ||
|
|
bc30db2875 | ||
|
|
abe914d446 | ||
|
|
cc65ce6812 | ||
|
|
1a6454ee79 | ||
|
|
b72f397369 | ||
|
|
e698fe8d18 | ||
|
|
c8cad1c295 |
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
custom:
|
||||
[
|
||||
"https://t.me/tribute/app?startapp=dtfk",
|
||||
"https://t.me/tribute/app?startapp=dtLE",
|
||||
]
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -61,5 +61,12 @@ body:
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||
value: |
|
||||
<details><summary>日志内容 / Log Content</summary>
|
||||
```log
|
||||
<!-- 在此处粘贴完整日志 / Paste the full log here -->
|
||||
|
||||
```
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
80
.github/workflows/autobuild.yml
vendored
@@ -2,9 +2,9 @@ name: Auto Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 0,6,12,18
|
||||
- cron: "0 16,22,4,10 * * *"
|
||||
# schedule:
|
||||
# # UTC+8 0,6,12,18
|
||||
# - cron: "0 16,22,4,10 * * *"
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: autobuild
|
||||
@@ -77,6 +77,15 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update git tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git tag -f ${{ env.TAG_NAME }}
|
||||
git push --force origin ${{ env.TAG_NAME }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
@@ -111,31 +120,25 @@ jobs:
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## 我应该下载哪个版本?
|
||||
## Which version should I download?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
- MacOS Intel Chip: x64.dmg
|
||||
- MacOS Apple M Chip: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
- Linux 64-bit: amd64.deb/amd64.rpm
|
||||
- Linux arm64: arm64.deb/aarch64.rpm
|
||||
- Linux armv7: armhf.deb/armhfp.rpm
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- 64位: x64-setup.exe
|
||||
- arm64架构: arm64-setup.exe
|
||||
#### 便携版问题很多不再提供
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- 64位: x64_fixed_webview2-setup.exe
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
### Windows (Win7 is no longer supported)
|
||||
#### Normal version (recommended)
|
||||
- 64-bit: x64-setup.exe
|
||||
- arm64: arm64-setup.exe
|
||||
#### Portable version is no longer available with many problems
|
||||
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
|
||||
- 64-bit: x64_fixed_webview2-setup.exe
|
||||
- arm64: arm64_fixed_webview2-setup.exe
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
@@ -150,6 +153,28 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
clean_old_assets:
|
||||
name: Clean Old Release Assets
|
||||
runs-on: ubuntu-latest
|
||||
needs: update_tag
|
||||
if: ${{ needs.update_tag.result == 'success' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Remove old assets from release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ env.TAG_NAME }}
|
||||
run: |
|
||||
VERSION=$(cat package.json | jq -r '.version')
|
||||
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
||||
for asset in $assets; do
|
||||
if [[ "$asset" != *"$VERSION"* ]]; then
|
||||
echo "Deleting old asset: $asset"
|
||||
gh release delete-asset "$TAG_NAME" "$asset" -y
|
||||
fi
|
||||
done
|
||||
|
||||
autobuild-x86-windows-macos-linux:
|
||||
name: Autobuild x86 Windows, MacOS and Linux
|
||||
needs: update_tag
|
||||
@@ -168,6 +193,9 @@ jobs:
|
||||
- os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -217,12 +245,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: ${{ env.TAG_NAME }}
|
||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
|
||||
3
.github/workflows/clippy.yml
vendored
@@ -17,6 +17,9 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
3
.github/workflows/dev.yml
vendored
@@ -28,6 +28,9 @@ jobs:
|
||||
bundle: dmg
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
7
.github/workflows/fmt.yml
vendored
@@ -10,6 +10,9 @@ on:
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_NET_RETRY: "5"
|
||||
CARGO_HTTP_CHECK_REVOKE: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -26,11 +29,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: corepack enable
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm format:check
|
||||
|
||||
# taplo:
|
||||
|
||||
255
.github/workflows/release.yml
vendored
@@ -40,9 +40,91 @@ jobs:
|
||||
fi
|
||||
echo "Tag and package.json version are consistent."
|
||||
|
||||
create_release_notes:
|
||||
name: Create Release Notes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
shell: bash
|
||||
|
||||
- name: Get Version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
||||
|
||||
- run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## Which version should I download?
|
||||
|
||||
### macOS
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_aarch64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Apple%20Silicon"></a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br>
|
||||
> :warning: **Warning**
|
||||
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br>
|
||||
<code>sudo xattr -r -c /Applications/Koala\ Clash.app</code>
|
||||
|
||||
### Linux
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
|
||||
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
|
||||
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
|
||||
|
||||
### Windows (Win7 is no longer supported)
|
||||
#### Normal version (recommended)
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
|
||||
#### Portable version is no longer available with many problems
|
||||
#### Built-in Webview version 2 (large size, only used in enterprise version of the system or can not install webview2)
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
|
||||
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64_fixed_webview2-setup.exe"><img src="https://badgen.net/badge/icon/arm64?icon=windows&label=exe"></a>
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{env.VERSION}}
|
||||
name: "Koala Clash v${{env.VERSION}}"
|
||||
body_path: release.txt
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release:
|
||||
name: Release Build
|
||||
needs: check_tag_version
|
||||
needs: [check_tag_version, create_release_notes]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -96,7 +178,13 @@ jobs:
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Create .p8 file
|
||||
run: |
|
||||
mkdir -p ~/.appstoreconnect/private_keys
|
||||
echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
|
||||
|
||||
- name: Tauri build
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -106,16 +194,61 @@ jobs:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: "~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: "Clash Verge Rev v__VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename Artifact (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
- name: Rename Artifact (Linux/macOS)
|
||||
if: runner.os == 'Linux' || runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
|
||||
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
|
||||
dir_path=$(dirname "$old_path")
|
||||
old_filename=$(basename "$old_path")
|
||||
new_filename=$(echo "$old_filename" \
|
||||
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
|
||||
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
|
||||
)
|
||||
new_path="${dir_path}/${new_filename}"
|
||||
if [ "$old_path" != "$new_path" ]; then
|
||||
echo " - '$old_filename' -> '$new_filename'"
|
||||
mv "$old_path" "$new_path"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{steps.build.outputs.appVersion}}
|
||||
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig
|
||||
|
||||
release-for-linux-arm:
|
||||
name: Release Build for Linux ARM
|
||||
strategy:
|
||||
@@ -225,14 +358,36 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
||||
|
||||
- name: Rename
|
||||
shell: bash
|
||||
run: |
|
||||
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
|
||||
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
|
||||
dir_path=$(dirname "$old_path")
|
||||
old_filename=$(basename "$old_path")
|
||||
new_filename=$(echo "$old_filename" \
|
||||
| sed -E 's/_[0-9]+\.[0-9]+\.[0-9]+_/_/' \
|
||||
| sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+//' \
|
||||
)
|
||||
new_path="${dir_path}/${new_filename}"
|
||||
if [ "$old_path" != "$new_path" ]; then
|
||||
echo " - '$old_filename' -> '$new_filename'"
|
||||
mv "$old_path" "$new_path"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{env.VERSION}}
|
||||
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||
body: "More new features are now supported."
|
||||
name: "Koala Clash v${{env.VERSION}}"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
@@ -281,8 +436,8 @@ jobs:
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
|
||||
Expand .\Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||
|
||||
@@ -302,19 +457,19 @@ jobs:
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
@@ -322,8 +477,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{steps.build.outputs.appVersion}}
|
||||
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
||||
body: "More new features are now supported."
|
||||
name: "Koala Clash v${{steps.build.outputs.appVersion}}"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
@@ -383,25 +537,68 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
submit-to-winget:
|
||||
name: Submit to Winget
|
||||
push-notify-to-telegram:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-update]
|
||||
needs: [release-update, release-update-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
shell: bash
|
||||
|
||||
- name: Get Version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
- name: Submit to Winget
|
||||
uses: vedantmgoyal9/winget-releaser@main
|
||||
echo "BUILDTIME=$(TZ=Europe/Moscow date)" >> $GITHUB_ENV
|
||||
|
||||
- run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/')
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
Вышло обновление!
|
||||
|
||||
$UPDATE_LOGS
|
||||
|
||||
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
|
||||
|
||||
EOF
|
||||
|
||||
- name: notify to channel
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
identifier: ClashVergeRev.ClashVergeRev
|
||||
version: ${{env.VERSION}}
|
||||
release-tag: v${{env.VERSION}}
|
||||
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
to: ${{ secrets.TELEGRAM_TO_CHANNEL }}
|
||||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
message_file: release.txt
|
||||
format: markdown
|
||||
|
||||
- name: notify to group
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_TO_GROUP }}
|
||||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
message_file: release.txt
|
||||
format: markdown
|
||||
|
||||
1
.gitignore
vendored
@@ -10,3 +10,4 @@ scripts/_env.sh
|
||||
.tool-versions
|
||||
.idea
|
||||
.old
|
||||
bun.lock
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# $1: remote name (e.g., origin)
|
||||
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
|
||||
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
||||
if [ $? -ne 0 ]; then
|
||||
@@ -8,11 +11,9 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
fi
|
||||
fi
|
||||
|
||||
remote_name="$1"
|
||||
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)"
|
||||
# 只在 push 到 origin 并且 origin 指向目标仓库时执行格式检查
|
||||
if [ "$1" = "origin" ] && echo "$2" | grep -Eq 'github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$'; then
|
||||
echo "[pre-push] Detected push to origin (clash-verge-rev/clash-verge-rev)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
|
||||
pnpm format:check
|
||||
|
||||
18
.prettierrc
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"experimentalOperatorPosition": "start"
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
|
||||
74
README.md
@@ -1,7 +1,7 @@
|
||||
<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>
|
||||
Fork of <a href="https://github.com/clash-verge-rev/clash-verge-rev">Clash Verge Rev</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
@@ -11,74 +11,26 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
|
||||
## Preview
|
||||
|
||||
| Dark | Light |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
|  |  |
|
||||
| Dark | Light |
|
||||
| ----------------------------------- | ------------------------------------ |
|
||||
|  |  |
|
||||
|
||||
## Install
|
||||
|
||||
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||
Go to the [Release page](https://github.com/coolcoala/clash-verge-rev-lite/releases) to download the corresponding installation package<br>
|
||||
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
#### 我应当怎样选择发行版
|
||||
|
||||
| 版本 | 特征 | 链接 |
|
||||
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||
|
||||
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
||||
|
||||
---
|
||||
|
||||
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
|
||||
|
||||
## Promotion
|
||||
|
||||
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||
- 海外团队,无跑路风险,高达 50% 返佣
|
||||
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
||||
|
||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
||||
|
||||
🧩 YXVM 独立服务器优势:
|
||||
|
||||
- 🌎 优质网络,回程优化,下载快到飞起
|
||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
||||
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
|
||||
- 📦 本项目使用配置已在售,欢迎同款入手!
|
||||
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
|
||||
### Telegram channel: ---
|
||||
|
||||
## Features
|
||||
|
||||
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
|
||||
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`。
|
||||
- 配置文件管理和增强(Merge 和 Script),配置文件语法提示。
|
||||
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
|
||||
- 可视化节点和规则编辑
|
||||
- WebDav 配置备份和同步
|
||||
|
||||
### FAQ
|
||||
|
||||
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### Donation
|
||||
|
||||
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
|
||||
- Based on the powerful Rust and Tauri 2 frameworks.
|
||||
- Built-in [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) kernel with support for switching between `Alpha` versions of the kernel.
|
||||
- Simple and beautiful user interface with support for custom theme colors, agent group/tray icons, and `CSS Injection`.
|
||||
- Configuration file management, configuration file syntax hints.
|
||||
- System Agent and Guard, `TUN (Virtual NIC)` mode.
|
||||
- Visual node and rule editing
|
||||
- WebDav configuration backup and synchronization
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
1808
UPDATELOG.md
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@root/src/components",
|
||||
"utils": "@root/lib/utils",
|
||||
"ui": "@root/src/components/ui",
|
||||
"lib": "@root/lib",
|
||||
"hooks": "@root/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 712 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 671 KiB |
21
hooks/use-mobile.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
72
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.3.1",
|
||||
"name": "koala-clash",
|
||||
"version": "0.2.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
@@ -30,53 +30,80 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/lab": "7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.2",
|
||||
"@mui/x-data-grid": "^8.5.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||
"@tauri-apps/plugin-notification": "^2.2.3",
|
||||
"@tauri-apps/plugin-process": "^2.2.2",
|
||||
"@tauri-apps/plugin-shell": "2.2.2",
|
||||
"@tauri-apps/plugin-updater": "2.8.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.3",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "2.2.1",
|
||||
"@tauri-apps/plugin-updater": "2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.5",
|
||||
"axios": "^1.10.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"axios": "^1.9.0",
|
||||
"chart.js": "^4.4.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cli-color": "^2.0.4",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.49",
|
||||
"glob": "^11.0.3",
|
||||
"foxact": "^0.2.45",
|
||||
"framer-motion": "^12.23.12",
|
||||
"glob": "^11.0.2",
|
||||
"i18next": "^25.2.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.514.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"peggy": "^5.0.3",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "15.5.3",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "15.5.2",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.2",
|
||||
"react-virtuoso": "^4.13.0",
|
||||
"react-virtuoso": "^4.12.8",
|
||||
"sockette": "^2.0.6",
|
||||
"sonner": "^2.0.5",
|
||||
"swr": "^2.3.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -85,21 +112,26 @@
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-react": "4.5.2",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"postcss": "^8.5.4",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"sass": "^1.89.2",
|
||||
"terser": "^5.43.0",
|
||||
"sass": "^1.89.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"terser": "^5.41.0",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
|
||||
2116
pnpm-lock.yaml
generated
@@ -42,16 +42,16 @@ async function resolvePortable() {
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "Koala Clash.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
zip.addLocalFolder(
|
||||
path.join(
|
||||
releaseDir,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
||||
),
|
||||
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||
`Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${arch}`,
|
||||
);
|
||||
zip.addLocalFolder(configDir, ".config");
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ async function resolvePortable() {
|
||||
}
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-clash.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
zip.addLocalFolder(configDir, ".config");
|
||||
|
||||
|
||||
@@ -175,8 +175,8 @@ function clashMetaAlpha() {
|
||||
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo-alpha",
|
||||
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
name: "koala-mihomo-alpha",
|
||||
targetFile: `koala-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
@@ -192,8 +192,8 @@ function clashMeta() {
|
||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo",
|
||||
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
name: "koala-mihomo",
|
||||
targetFile: `koala-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
@@ -381,7 +381,7 @@ const resolvePlugin = async () => {
|
||||
// service chmod
|
||||
const resolveServicePermission = async () => {
|
||||
const serviceExecutables = [
|
||||
"clash-verge-service*",
|
||||
"koala-clash-service*",
|
||||
"install-service*",
|
||||
"uninstall-service*",
|
||||
];
|
||||
@@ -429,14 +429,14 @@ async function resolveLocales() {
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||
const SERVICE_URL = `https://github.com/coolcoala/koala-clash-service/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
resolveResource({
|
||||
file: "clash-verge-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||
file: "koala-clash-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/koala-clash-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -489,13 +489,13 @@ const resolveWinSysproxy = () =>
|
||||
const tasks = [
|
||||
// { name: "clash", func: resolveClash, retry: 5 },
|
||||
{
|
||||
name: "verge-mihomo-alpha",
|
||||
name: "koala-mihomo-alpha",
|
||||
func: () =>
|
||||
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||
retry: 5,
|
||||
},
|
||||
{
|
||||
name: "verge-mihomo",
|
||||
name: "koala-mihomo",
|
||||
func: () =>
|
||||
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||
retry: 5,
|
||||
|
||||
2160
src-tauri/Cargo.lock
generated
@@ -1,21 +1,24 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.3.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
name = "koala-clash"
|
||||
version = "0.2.6"
|
||||
description = "koala clash"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
|
||||
default-run = "koala-clash"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
identifier = "io.github.koala-clash"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
tauri-build = { version = "2.3.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
url = "2.5.4"
|
||||
os_info = "3.0"
|
||||
machine-uid = "0.5.3"
|
||||
warp = "0.3.7"
|
||||
anyhow = "1.0.98"
|
||||
dirs = "6.0"
|
||||
@@ -25,7 +28,7 @@ dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = "0.35.2"
|
||||
sysinfo = "0.36.1"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34-deprecated"
|
||||
@@ -42,12 +45,12 @@ tokio = { version = "1.45.1", features = [
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies", "brotli", "gzip", "zstd"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
image = "0.25.6"
|
||||
imageproc = "0.25.0"
|
||||
tauri = { version = "2.5.1", features = [
|
||||
tauri = { version = "2.6.2", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -55,15 +58,14 @@ tauri = { version = "2.5.1", features = [
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.2"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-fs = "2.3.0"
|
||||
tauri-plugin-process = "2.2.2"
|
||||
tauri-plugin-clipboard-manager = "2.2.3"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-shell = "2.3.0"
|
||||
tauri-plugin-dialog = "2.3.0"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-window-state = "2.2.3"
|
||||
zip = "4.1.0"
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
zip = "4.2.0"
|
||||
reqwest_dav = "0.2.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
@@ -75,11 +77,14 @@ async-trait = "0.1.88"
|
||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||
ab_glyph = "0.2.29"
|
||||
tungstenite = "0.27.0"
|
||||
libc = "0.2.173"
|
||||
libc = "0.2.174"
|
||||
gethostname = "1.0.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.0"
|
||||
tauri-plugin-deep-link = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -93,15 +98,20 @@ winapi = { version = "0.3.9", features = [
|
||||
"errhandlingapi",
|
||||
"minwindef",
|
||||
"winerror",
|
||||
"tlhelp32",
|
||||
"processthreadsapi",
|
||||
"winhttp",
|
||||
"winreg",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.4.0"
|
||||
tauri-plugin-global-shortcut = "2.2.1"
|
||||
tauri-plugin-updater = "2.8.1"
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
tauri-plugin-global-shortcut = "2.3.0"
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:window:allow-set-theme"
|
||||
"core:window:allow-set-theme",
|
||||
"notification:default",
|
||||
"core:webview:allow-set-webview-zoom"
|
||||
]
|
||||
}
|
||||
|
||||
236
src-tauri/deny.toml
Normal file
@@ -0,0 +1,236 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
"RUSTSEC-2024-0415",
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
#"MIT",
|
||||
#"Apache-2.0",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.85
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# github.com organizations to allow git sources for
|
||||
github = []
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 40 KiB |
BIN
src-tauri/icons/tray-icon-sys-mono-new.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 68 KiB |
BIN
src-tauri/icons/tray-icon-tun-mono-new.ico
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
chmod +x /usr/bin/install-service
|
||||
chmod +x /usr/bin/uninstall-service
|
||||
chmod +x /usr/bin/clash-verge-service
|
||||
chmod +x /usr/bin/koala-clash-service
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<false/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>io.github.clash-verge-rev.clash-verge-rev</string>
|
||||
<string>io.github.koala-clash</string>
|
||||
</array>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
|
||||
@@ -427,52 +427,52 @@ Function .onInit
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
!macro CheckAllVergeProcesses
|
||||
; Check if clash-verge-service.exe is running
|
||||
!macro CheckAllKoalaProcesses
|
||||
; Check if koala-clash-service.exe is running
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "clash-verge-service.exe"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "koala-clash-service.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
|
||||
nsis_tauri_utils::FindProcess "koala-clash-service.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
DetailPrint "Kill clash-verge-service.exe..."
|
||||
DetailPrint "Kill koala-clash-service.exe..."
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "koala-clash-service.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
|
||||
nsis_tauri_utils::KillProcess "koala-clash-service.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
; Check if verge-mihomo-alpha.exe is running
|
||||
; Check if koala-mihomo-alpha.exe is running
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo-alpha.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::FindProcess "koala-mihomo-alpha.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
DetailPrint "Kill verge-mihomo-alpha.exe..."
|
||||
DetailPrint "Kill koala-mihomo-alpha.exe..."
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo-alpha.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "verge-mihomo-alpha.exe"
|
||||
nsis_tauri_utils::KillProcess "koala-mihomo-alpha.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
; Check if verge-mihomo.exe is running
|
||||
; Check if koala-mihomo.exe is running
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo.exe"
|
||||
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo.exe"
|
||||
!else
|
||||
nsis_tauri_utils::FindProcess "verge-mihomo.exe"
|
||||
nsis_tauri_utils::FindProcess "koala-mihomo.exe"
|
||||
!endif
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
DetailPrint "Kill verge-mihomo.exe..."
|
||||
DetailPrint "Kill koala-mihomo.exe..."
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo.exe"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo.exe"
|
||||
!else
|
||||
nsis_tauri_utils::KillProcess "verge-mihomo.exe"
|
||||
nsis_tauri_utils::KillProcess "koala-mihomo.exe"
|
||||
!endif
|
||||
${EndIf}
|
||||
|
||||
@@ -509,22 +509,22 @@ FunctionEnd
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro StartVergeService
|
||||
!macro StartKoalaService
|
||||
; Check if the service exists
|
||||
SimpleSC::ExistsService "clash_verge_service"
|
||||
SimpleSC::ExistsService "koala_clash_service"
|
||||
Pop $0 ; 0:service exists;other: service not exists
|
||||
; Service exists
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
; Check if the service is running
|
||||
SimpleSC::ServiceIsRunning "clash_verge_service"
|
||||
SimpleSC::ServiceIsRunning "koala_clash_service"
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
${If} $1 == 0
|
||||
DetailPrint "Restart Clash Verge Service..."
|
||||
SimpleSC::StartService "clash_verge_service" "" 30
|
||||
DetailPrint "Restart Koala Clash Service..."
|
||||
SimpleSC::StartService "koala_clash_service" "" 30
|
||||
${EndIf}
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
@@ -535,35 +535,35 @@ FunctionEnd
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro RemoveVergeService
|
||||
!macro RemoveKoalaService
|
||||
; Check if the service exists
|
||||
SimpleSC::ExistsService "clash_verge_service"
|
||||
SimpleSC::ExistsService "koala_clash_service"
|
||||
Pop $0 ; 0:service exists;other: service not exists
|
||||
; Service exists
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
; Check if the service is running
|
||||
SimpleSC::ServiceIsRunning "clash_verge_service"
|
||||
SimpleSC::ServiceIsRunning "koala_clash_service"
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
${If} $1 == 1
|
||||
DetailPrint "Stop Clash Verge Service..."
|
||||
SimpleSC::StopService "clash_verge_service" 1 30
|
||||
DetailPrint "Stop Koala Clash Service..."
|
||||
SimpleSC::StopService "koala_clash_service" 1 30
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
${If} $0 == 0
|
||||
DetailPrint "Removing Clash Verge Service..."
|
||||
SimpleSC::RemoveService "clash_verge_service"
|
||||
DetailPrint "Removing Koala Clash Service..."
|
||||
SimpleSC::RemoveService "koala_clash_service"
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
SimpleSC::GetErrorMessage
|
||||
Pop $0
|
||||
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
|
||||
MessageBox MB_OK|MB_ICONSTOP "Koala Clash Service Stop Error ($0)"
|
||||
${EndIf}
|
||||
${ElseIf} $1 == 0
|
||||
DetailPrint "Removing Clash Verge Service..."
|
||||
SimpleSC::RemoveService "clash_verge_service"
|
||||
DetailPrint "Removing Koala Clash Service..."
|
||||
SimpleSC::RemoveService "koala_clash_service"
|
||||
${EndIf}
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
@@ -764,7 +764,7 @@ Section Install
|
||||
SetOutPath $INSTDIR
|
||||
nsExec::Exec 'netsh int tcp res'
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro CheckAllKoalaProcesses
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
@@ -772,32 +772,32 @@ Section Install
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
; 清理旧版本的注册表项 (Koala Clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
DeleteRegValue HKCU "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
DeleteRegValue HKLM "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
; 清理新版本的注册表项 (clash-verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "clash-verge"
|
||||
; 清理新版本的注册表项 (koala-clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
DeleteRegValue HKCU "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
DeleteRegValue HKLM "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
; Delete old files before installation
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
; Delete koala-clash.desktop
|
||||
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
|
||||
Delete "$INSTDIR\Koala Clash.exe"
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
@@ -815,7 +815,7 @@ Section Install
|
||||
File /a "/oname={{this}}" "{{@key}}"
|
||||
{{/each}}
|
||||
|
||||
!insertmacro StartVergeService
|
||||
!insertmacro StartKoalaService
|
||||
|
||||
; Create uninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
@@ -918,11 +918,11 @@ FunctionEnd
|
||||
Section Uninstall
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.koala-clash\window-state.json"
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro RemoveVergeService
|
||||
!insertmacro CheckAllKoalaProcesses
|
||||
!insertmacro RemoveKoalaService
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
@@ -930,26 +930,26 @@ Section Uninstall
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
; 清理旧版本的注册表项 (Koala Clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
DeleteRegValue HKCU "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "Koala Clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
DeleteRegValue HKLM "$R1" "Koala Clash"
|
||||
${EndIf}
|
||||
|
||||
; 清理新版本的注册表项 (clash-verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "clash-verge"
|
||||
; 清理新版本的注册表项 (koala-clash)
|
||||
ReadRegStr $R2 HKCU "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
DeleteRegValue HKCU "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
ReadRegStr $R2 HKLM "$R1" "koala-clash"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
DeleteRegValue HKLM "$R1" "koala-clash"
|
||||
${EndIf}
|
||||
|
||||
; Delete the app directory and its content from disk
|
||||
@@ -966,9 +966,9 @@ Section Uninstall
|
||||
Delete "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
; Delete koala-clash.desktop
|
||||
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
|
||||
Delete "$INSTDIR\Koala Clash.exe"
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
@@ -982,20 +982,20 @@ Section Uninstall
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\koala-clash.lnk"
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
|
||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
|
||||
; Remove desktop shortcuts
|
||||
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$DESKTOP\clash-verge.lnk"
|
||||
Delete "$DESKTOP\koala-clash.lnk"
|
||||
|
||||
; Remove registry information for add/remove programs
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
@@ -1017,7 +1017,7 @@ Section Uninstall
|
||||
|
||||
;删除 window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.koala-clash\window-state.json"
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $R0
|
||||
IfErrors +2 0
|
||||
|
||||
@@ -147,7 +147,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {}", url))
|
||||
Err(format!("Downloaded content is not a valid image: {url}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,15 +209,17 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
log::info!(target: "app", "前端UI已准备就绪");
|
||||
log::info!(target: "app", "Frontend UI is ready");
|
||||
crate::utils::resolve::mark_ui_ready();
|
||||
// Flush any pending messages queued while UI was not ready (e.g. minimized to tray)
|
||||
crate::core::handle::Handle::global().flush_ui_pending_messages();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
log::info!(target: "app", "UI加载阶段更新: {}", stage);
|
||||
log::info!(target: "app", "UI loading stage updated: {stage}");
|
||||
|
||||
use crate::utils::resolve::UiReadyStage;
|
||||
|
||||
@@ -228,8 +230,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||
"Ready" => UiReadyStage::Ready,
|
||||
_ => {
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
|
||||
return Err(format!("未知的UI加载阶段: {}", stage));
|
||||
log::warn!(target: "app", "Unknown UI loading stage: {stage}");
|
||||
return Err(format!("Unknown UI loading stage: {stage}"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -240,7 +242,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
/// 重置UI就绪状态
|
||||
#[tauri::command]
|
||||
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
||||
log::info!(target: "app", "重置UI就绪状态");
|
||||
log::info!(target: "app", "Reset UI ready state");
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Core changed but failed to restart: {}", err);
|
||||
log::error!(target: "app", "{}", error_msg);
|
||||
let error_msg = format!("Core changed but failed to restart: {err}");
|
||||
log::error!(target: "app", "{error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
@@ -116,7 +116,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
||||
log::info!(target: "app", "DNS config saved to {dns_path:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -137,7 +137,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
let dns_path = match dirs::app_home_dir() {
|
||||
Ok(path) => path.join("dns_config.yaml"),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get home dir: {}", e);
|
||||
log::error!(target: "app", "Failed to get home dir: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -150,7 +150,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
||||
log::error!(target: "app", "Failed to read DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -163,7 +163,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
patch
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||
log::error!(target: "app", "Failed to parse DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -178,13 +178,13 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
|
||||
// 首先重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
||||
log::error!(target: "app", "Failed to regenerate config with DNS: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 然后应用新配置
|
||||
if let Err(err) = CoreManager::global().update_config().await {
|
||||
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
||||
log::error!(target: "app", "Failed to apply config with DNS: {err}");
|
||||
} else {
|
||||
log::info!(target: "app", "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -196,7 +196,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||
log::error!(target: "app", "Failed to regenerate config: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn country_code_to_emoji(country_code: &str) -> String {
|
||||
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)))
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
|
||||
map.get("loc").map(|loc| {
|
||||
let emoji = country_code_to_emoji(loc);
|
||||
format!("{}{}", emoji, loc)
|
||||
format!("{emoji}{loc}")
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -255,7 +255,7 @@ async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -384,10 +384,8 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
|
||||
let url = format!(
|
||||
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}",
|
||||
device_id
|
||||
);
|
||||
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) => {
|
||||
@@ -431,7 +429,7 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
.map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
}
|
||||
Err(_) => None,
|
||||
@@ -470,7 +468,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
|
||||
// 检查连接失败情况
|
||||
if let Err(e) = &result1 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
@@ -487,7 +485,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
@@ -541,7 +539,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region_code)),
|
||||
region: Some(format!("{emoji}{region_code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -557,7 +555,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("获取Netflix区域信息失败: {}", e);
|
||||
eprintln!("获取Netflix区域信息失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
@@ -570,7 +568,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
// 其他未知错误状态
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (状态码: {}_{}", status1, status2),
|
||||
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
@@ -614,7 +612,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, country)),
|
||||
region: Some(format!("{emoji}{country}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -631,7 +629,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("解析Fast.com API响应失败: {}", e);
|
||||
eprintln!("解析Fast.com API响应失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
@@ -642,7 +640,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fast.com API请求失败: {}", e);
|
||||
eprintln!("Fast.com API请求失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
@@ -884,7 +882,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -947,7 +945,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -968,7 +966,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -990,7 +988,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Soon".to_string(),
|
||||
region: Some(format!("{}{}(即将上线)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
@@ -999,13 +997,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
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),
|
||||
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
@@ -1056,7 +1054,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -1170,7 +1168,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
.connection_verbose(true) // 详细连接信息
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||
};
|
||||
|
||||
// 创建共享的结果集
|
||||
@@ -1284,7 +1282,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
// 等待所有任务完成
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
if let Err(e) = res {
|
||||
eprintln!("任务执行失败: {}", e);
|
||||
eprintln!("任务执行失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = spawn_blocking(Sysproxy::get_system_proxy)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))?
|
||||
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
|
||||
log::debug!(target: "app", "Asynchronously getting system proxy configuration");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
@@ -21,21 +19,28 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
log::debug!(target: "app", "Return system proxy configuration: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = spawn_blocking(Autoproxy::get_auto_proxy)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
|
||||
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
|
||||
log::debug!(target: "app", "Start retrieving auto proxy configuration (event-driven)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
let current = proxy_manager.get_auto_proxy_cached();
|
||||
// 异步请求更新,立即返回缓存数据
|
||||
tokio::spawn(async move {
|
||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||
});
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
|
||||
log::debug!(target: "app", "Return auto proxy configuration (cached): enable={}, url={}", current.enable, current.url);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
@@ -49,7 +54,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
|
||||
Ok(name) => name,
|
||||
Err(os_string) => {
|
||||
// 对于包含非UTF-8的主机名,使用调试格式化
|
||||
let fallback = format!("{:?}", os_string);
|
||||
let fallback = format!("{os_string:?}");
|
||||
// 去掉可能存在的引号
|
||||
fallback.trim_matches('"').to_string()
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
state.proxies = Box::new(proxies);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "proxies刷新成功");
|
||||
log::debug!(target: "app", "Proxies refreshed successfully");
|
||||
}
|
||||
|
||||
let proxies = {
|
||||
@@ -50,7 +50,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存");
|
||||
log::debug!(target: "app", "Force refresh proxy cache");
|
||||
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
|
||||
@@ -61,7 +61,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
state.last_refresh_time = Instant::now();
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "强制刷新代理缓存完成");
|
||||
log::debug!(target: "app", "Force refresh proxy cache completed");
|
||||
Ok(proxies)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
state.providers_proxies = Box::new(providers);
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "providers_proxies刷新成功");
|
||||
log::debug!(target: "app", "providers_proxies refreshed successfully");
|
||||
}
|
||||
|
||||
let providers_proxies = {
|
||||
|
||||
@@ -84,7 +84,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "Merge config file");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -133,17 +133,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
log::info!(target: "app", "[cmd config save] YAML config file validation failed, sending notification");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML config file");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
log::info!(target: "app", "[cmd config save] Script file validation failed, sending notification");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
log::info!(target: "app", "[cmd config save] Other validation failure type, sending general notification");
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证过程发生错误: {}",
|
||||
"[cmd config save] Error occurred during validation: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
|
||||
@@ -24,7 +24,7 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
let info = format!("{sysinfo:?}");
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cliboard = app_handle.clipboard();
|
||||
@@ -37,7 +37,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
let info = format!("{sysinfo:?}");
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"{} 验证失败: {}",
|
||||
"{} validation failed: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
@@ -43,14 +43,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
|
||||
logging!(info, Type::Config, true, "Validating script file: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
handle_script_validation_notice(&result, "Script file");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -129,7 +129,7 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[通知] 发送通知: status={}, msg={}",
|
||||
"[Notice] Sending notice: status={}, msg={}",
|
||||
status,
|
||||
error_msg
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ impl IClashTemp {
|
||||
tun.insert("enable".into(), false.into());
|
||||
tun.insert("stack".into(), "gvisor".into());
|
||||
tun.insert("auto-route".into(), true.into());
|
||||
tun.insert("strict-route".into(), false.into());
|
||||
tun.insert("strict-route".into(), true.into());
|
||||
tun.insert("auto-detect-interface".into(), true.into());
|
||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -129,7 +129,7 @@ impl IClashTemp {
|
||||
help::save_yaml(
|
||||
&dirs::clash_path()?,
|
||||
&self.0,
|
||||
Some("# Generated by Clash Verge"),
|
||||
Some("# Generated by Koala Clash"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
pub const RUNTIME_CONFIG: &str = "koala-clash.yaml";
|
||||
pub const CHECK_CONFIG: &str = "koala-clash-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<Box<IClashTemp>>,
|
||||
@@ -69,9 +69,9 @@ impl Config {
|
||||
}
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||
logging!(error, Type::Config, true, "Failed to generate runtime config: {}", err);
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||
logging!(info, Type::Config, true, "Runtime config generated successfully");
|
||||
}
|
||||
|
||||
// 生成运行时配置文件并验证
|
||||
@@ -79,7 +79,7 @@ impl Config {
|
||||
|
||||
let validation_result = if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
logging!(info, Type::Config, true, "开始验证配置");
|
||||
logging!(info, Type::Config, true, "Starting config validation");
|
||||
|
||||
match CoreManager::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
@@ -88,7 +88,7 @@ impl Config {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||
"[First launch] Config validation failed, starting with minimal default config: {}",
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
@@ -96,12 +96,12 @@ impl Config {
|
||||
.await?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "配置验证成功");
|
||||
logging!(info, Type::Config, true, "Config validation succeeded");
|
||||
Some(("config_validate::success", String::new()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||
logging!(warn, Type::Config, true, "Validation process execution failed: {}", err);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
@@ -109,7 +109,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||
logging!(warn, Type::Config, true, "Failed to generate config file; using default config");
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
@@ -141,7 +141,7 @@ impl Config {
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
||||
help::save_yaml(&path, &config, Some("# Generated by Koala Clash"))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ macro_rules! draft_define {
|
||||
|
||||
impl Draft<Box<$id>> {
|
||||
#[allow(unused)]
|
||||
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
pub fn data(&self) -> MappedMutexGuard<'_, Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||
}
|
||||
|
||||
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
pub fn latest(&self) -> MappedMutexGuard<'_, Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
&mut inner.0
|
||||
@@ -33,7 +33,7 @@ macro_rules! draft_define {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
|
||||
pub fn draft(&self) -> MappedMutexGuard<'_, Box<$id>> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Encrypt data
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
||||
.map_err(|e| format!("Encryption failed: {}", e))?;
|
||||
.map_err(|e| format!("Encryption failed: {e}"))?;
|
||||
|
||||
// Concatenate nonce and ciphertext and encode them in base64
|
||||
let mut combined = nonce;
|
||||
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
||||
// Decrypt data
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce.into(), ciphertext)
|
||||
.map_err(|e| format!("Decryption failed: {}", e))?;
|
||||
.map_err(|e| format!("Decryption failed: {e}"))?;
|
||||
|
||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ use crate::utils::{
|
||||
tmpl,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, time::Duration};
|
||||
use url::Url;
|
||||
|
||||
use super::Config;
|
||||
|
||||
@@ -53,6 +55,18 @@ pub struct PrfItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub home: Option<String>,
|
||||
|
||||
/// profile support url
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub support_url: Option<String>,
|
||||
|
||||
/// profile announce
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub announce: Option<String>,
|
||||
|
||||
/// profile announce url
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub announce_url: Option<String>,
|
||||
|
||||
/// the file data
|
||||
#[serde(skip)]
|
||||
pub file_data: Option<String>,
|
||||
@@ -113,6 +127,12 @@ pub struct PrfOption {
|
||||
pub proxies: Option<String>,
|
||||
|
||||
pub groups: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_hwid: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub update_always: Option<bool>,
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
@@ -132,6 +152,8 @@ impl PrfOption {
|
||||
a.proxies = b.proxies.or(a.proxies);
|
||||
a.groups = b.groups.or(a.groups);
|
||||
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||
a.use_hwid = b.use_hwid.or(a.use_hwid);
|
||||
a.update_always = b.update_always.or(a.update_always);
|
||||
Some(a)
|
||||
}
|
||||
t => t.0.or(t.1),
|
||||
@@ -230,6 +252,9 @@ impl PrfItem {
|
||||
..PrfOption::default()
|
||||
}),
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||
})
|
||||
@@ -251,6 +276,7 @@ impl PrfItem {
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||
let use_hwid = Config::verge().latest().enable_send_hwid.unwrap_or(true);
|
||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||
@@ -274,6 +300,7 @@ impl PrfItem {
|
||||
Some(timeout),
|
||||
user_agent.clone(),
|
||||
accept_invalid_certs,
|
||||
use_hwid,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -291,6 +318,21 @@ impl PrfItem {
|
||||
|
||||
let header = resp.headers();
|
||||
|
||||
let mut final_url = url.to_string();
|
||||
|
||||
if let Some(new_domain_value) = header.get("new-sub-domain") {
|
||||
if let Ok(new_domain) = new_domain_value.to_str() {
|
||||
if !new_domain.is_empty() {
|
||||
if let Ok(mut parsed_url) = Url::parse(url) {
|
||||
if parsed_url.set_host(Some(new_domain)).is_ok() {
|
||||
final_url = parsed_url.to_string();
|
||||
log::info!(target: "app", "URL host updated to -> {final_url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse the Subscription UserInfo
|
||||
let extra = match header.get("Subscription-Userinfo") {
|
||||
Some(value) => {
|
||||
@@ -340,6 +382,11 @@ impl PrfItem {
|
||||
},
|
||||
};
|
||||
|
||||
let update_always = match header.get("update-always") {
|
||||
Some(value) => value.to_str().unwrap_or("false").parse::<bool>().ok(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let home = match header.get("profile-web-page-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
@@ -348,9 +395,64 @@ impl PrfItem {
|
||||
None => None,
|
||||
};
|
||||
|
||||
let support_url = match header.get("support-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
Some(str_value.to_string())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let announce = match header.get("announce") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||
STANDARD
|
||||
.decode(b64_data)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
} else {
|
||||
Some(str_value.to_string())
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(announce_msg) = &announce {
|
||||
let lower_msg = announce_msg.to_lowercase();
|
||||
if lower_msg.contains("device") || lower_msg.contains("устройств") {
|
||||
bail!(announce_msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let announce_url = match header.get("announce-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
Some(str_value.to_string())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let profile_title = match header.get("profile-title") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||
STANDARD
|
||||
.decode(b64_data)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
} else {
|
||||
Some(str_value.to_string())
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let uid = help::get_uid("R");
|
||||
let file = format!("{uid}.yaml");
|
||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||
let name = name
|
||||
.or(profile_title)
|
||||
.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||
let data = resp.text_with_charset("utf-8").await?;
|
||||
|
||||
// process the charset "UTF-8 with BOM"
|
||||
@@ -398,19 +500,33 @@ impl PrfItem {
|
||||
name: Some(name),
|
||||
desc,
|
||||
file: Some(file),
|
||||
url: Some(url.into()),
|
||||
url: Some(final_url),
|
||||
selected: None,
|
||||
extra,
|
||||
option: Some(PrfOption {
|
||||
user_agent: user_agent.clone(),
|
||||
with_proxy: if with_proxy { Some(true) } else { None },
|
||||
self_proxy: if self_proxy { Some(true) } else { None },
|
||||
update_interval,
|
||||
update_always,
|
||||
timeout_seconds: Some(timeout),
|
||||
danger_accept_invalid_certs: if accept_invalid_certs {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
merge,
|
||||
script,
|
||||
rules,
|
||||
proxies,
|
||||
groups,
|
||||
use_hwid: Some(use_hwid),
|
||||
..PrfOption::default()
|
||||
}),
|
||||
home,
|
||||
support_url,
|
||||
announce,
|
||||
announce_url,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(data.into()),
|
||||
})
|
||||
@@ -438,6 +554,9 @@ impl PrfItem {
|
||||
extra: None,
|
||||
option: None,
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(template),
|
||||
})
|
||||
@@ -460,6 +579,9 @@ impl PrfItem {
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
@@ -481,6 +603,9 @@ impl PrfItem {
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
@@ -502,6 +627,9 @@ impl PrfItem {
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
@@ -523,6 +651,9 @@ impl PrfItem {
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
support_url: None,
|
||||
announce: None,
|
||||
announce_url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
|
||||
@@ -66,7 +66,7 @@ impl IProfiles {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
self,
|
||||
Some("# Profiles Config for Clash Verge"),
|
||||
Some("# Profiles Config for Koala Clash"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,15 +131,14 @@ impl IProfiles {
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
if self.current.is_none()
|
||||
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
|
||||
{
|
||||
self.current = uid;
|
||||
if item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()) {
|
||||
// Always switch current to the newly created remote/local profile
|
||||
self.current = uid.clone();
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
@@ -220,6 +219,11 @@ impl IProfiles {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
each.home = item.home;
|
||||
each.announce = item.announce;
|
||||
each.announce_url = item.announce_url;
|
||||
each.support_url = item.support_url;
|
||||
each.name = item.name;
|
||||
each.url = item.url;
|
||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
@@ -234,9 +238,9 @@ impl IProfiles {
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -531,7 +535,7 @@ impl IProfiles {
|
||||
if Self::is_profile_file(file_name) {
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
|
||||
log::debug!(target: "app", "Protect global extension config file: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -540,11 +544,11 @@ impl IProfiles {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.to_string());
|
||||
log::info!(target: "app", "已清理冗余文件: {}", file_name);
|
||||
log::info!(target: "app", "Cleaned up redundant file: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{}: {}", file_name, e));
|
||||
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
|
||||
failed_deletions.push(format!("{file_name}: {e}"));
|
||||
log::warn!(target: "app", "Failed to clean file: {file_name} - {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -674,14 +678,14 @@ impl IProfiles {
|
||||
if !result.deleted_files.is_empty() {
|
||||
log::info!(
|
||||
target: "app",
|
||||
"自动清理完成,删除了 {} 个冗余文件",
|
||||
"Auto cleanup completed, deleted {} redundant files",
|
||||
result.deleted_files.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "自动清理失败: {}", e);
|
||||
log::warn!(target: "app", "Auto cleanup failed: {e}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ pub struct IVerge {
|
||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
pub enable_send_hwid: Option<bool>,
|
||||
|
||||
pub primary_action: Option<String>,
|
||||
|
||||
/// always use default bypass
|
||||
pub use_default_bypass: Option<bool>,
|
||||
|
||||
@@ -234,7 +238,7 @@ pub struct IVergeTheme {
|
||||
|
||||
impl IVerge {
|
||||
/// 有效的clash核心名称
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["koala-mihomo", "koala-mihomo-alpha"];
|
||||
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub fn validate_and_fix_config() -> Result<()> {
|
||||
@@ -253,10 +257,10 @@ impl IVerge {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
||||
"Invalid clash_core config detected at startup: '{}', auto-fixing to 'koala-mihomo'",
|
||||
core
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
config.clash_core = Some("koala-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
} else {
|
||||
@@ -264,21 +268,21 @@ impl IVerge {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
||||
"clash_core not configured at startup; setting default to 'koala-mihomo'"
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
config.clash_core = Some("koala-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
|
||||
// 修正后保存配置
|
||||
if needs_fix {
|
||||
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
||||
logging!(info, Type::Config, true, "Saving fixed configuration file...");
|
||||
help::save_yaml(&config_path, &config, Some("# Koala Clash Config"))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"配置文件修正完成,需要重新加载配置"
|
||||
"Configuration file fixed; reloading config required"
|
||||
);
|
||||
|
||||
Self::reload_config_after_fix(config)?;
|
||||
@@ -287,7 +291,7 @@ impl IVerge {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"clash_core配置验证通过: {:?}",
|
||||
"clash_core config validation passed: {:?}",
|
||||
config.clash_core
|
||||
);
|
||||
}
|
||||
@@ -317,7 +321,7 @@ impl IVerge {
|
||||
pub fn get_valid_clash_core(&self) -> String {
|
||||
self.clash_core
|
||||
.clone()
|
||||
.unwrap_or_else(|| "verge-mihomo".to_string())
|
||||
.unwrap_or_else(|| "koala-mihomo".to_string())
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
@@ -336,18 +340,17 @@ impl IVerge {
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
dirs::verge_path()
|
||||
.and_then(|path| help::read_yaml::<IVerge>(&path))
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
clash_core: Some("verge-mihomo".into()),
|
||||
clash_core: Some("koala-mihomo".into()),
|
||||
language: Some(Self::get_system_language()),
|
||||
theme_mode: Some("system".into()),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -391,7 +394,7 @@ impl IVerge {
|
||||
auto_close_connection: Some(true),
|
||||
auto_check_update: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
auto_log_clean: Some(2),
|
||||
webdav_url: None,
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
@@ -401,6 +404,8 @@ impl IVerge {
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
enable_dns_settings: Some(false),
|
||||
enable_send_hwid: Some(true),
|
||||
primary_action: Some("tun-mode".into()),
|
||||
home_cards: None,
|
||||
service_state: None,
|
||||
..Self::default()
|
||||
@@ -409,7 +414,7 @@ impl IVerge {
|
||||
|
||||
/// Save IVerge App Config
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Koala Clash Config"))
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
@@ -489,6 +494,8 @@ impl IVerge {
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(enable_send_hwid);
|
||||
patch!(primary_action);
|
||||
patch!(home_cards);
|
||||
patch!(service_state);
|
||||
}
|
||||
@@ -584,6 +591,8 @@ pub struct IVergeResponse {
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub enable_send_hwid: Option<bool>,
|
||||
pub primary_action: Option<String>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
@@ -656,6 +665,8 @@ impl From<IVerge> for IVergeResponse {
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
enable_send_hwid: verge.enable_send_hwid,
|
||||
primary_action: verge.primary_action,
|
||||
home_cards: verge.home_cards,
|
||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||
service_state: verge.service_state,
|
||||
|
||||
531
src-tauri/src/core/async_proxy_query.rs
Normal file
@@ -0,0 +1,531 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use anyhow::anyhow;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AsyncAutoproxy {
|
||||
pub enable: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AsyncSysproxy {
|
||||
pub enable: bool,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub bypass: String,
|
||||
}
|
||||
|
||||
impl Default for AsyncSysproxy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable: false,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 7890,
|
||||
bypass: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsyncProxyQuery;
|
||||
|
||||
impl AsyncProxyQuery {
|
||||
/// 异步获取自动代理配置(PAC)
|
||||
pub async fn get_auto_proxy() -> AsyncAutoproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
log::debug!(target: "app", "Async auto proxy fetch succeeded: enable={}, url={}", proxy.enable, proxy.url);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "Async auto proxy fetch failed: {e}");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "Async auto proxy fetch timed out");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步获取系统代理配置
|
||||
pub async fn get_system_proxy() -> AsyncSysproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
log::debug!(target: "app", "Async system proxy fetch succeeded: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "Async system proxy fetch failed: {e}");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "Async system proxy fetch timed out");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// Windows: 从注册表读取PAC配置
|
||||
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
||||
Self::get_pac_config_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
||||
|
||||
unsafe {
|
||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||
.encode_utf16()
|
||||
.collect::<Vec<u16>>();
|
||||
|
||||
let mut hkey: HKEY = ptr::null_mut();
|
||||
let result =
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
log::debug!(target: "app", "Unable to open registry key");
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
|
||||
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut url_buffer = vec![0u16; 1024];
|
||||
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
|
||||
let mut url_value_type: DWORD = 0;
|
||||
|
||||
let url_query_result = RegQueryValueExW(
|
||||
hkey,
|
||||
auto_config_url_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut url_value_type,
|
||||
url_buffer.as_mut_ptr() as *mut u8,
|
||||
&mut url_buffer_size,
|
||||
);
|
||||
|
||||
let mut pac_url = String::new();
|
||||
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
|
||||
let end_pos = url_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(url_buffer.len());
|
||||
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
||||
log::debug!(target: "app", "Read PAC URL from registry: {}", pac_url);
|
||||
}
|
||||
|
||||
// 2. 检查自动检测设置是否启用
|
||||
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut auto_detect: DWORD = 0;
|
||||
let mut detect_buffer_size: DWORD = 4;
|
||||
let mut detect_value_type: DWORD = 0;
|
||||
|
||||
let detect_query_result = RegQueryValueExW(
|
||||
hkey,
|
||||
auto_detect_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut detect_value_type,
|
||||
&mut auto_detect as *mut DWORD as *mut u8,
|
||||
&mut detect_buffer_size,
|
||||
);
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
|
||||
let pac_enabled = !pac_url.is_empty()
|
||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||
|
||||
if pac_enabled {
|
||||
log::debug!(target: "app", "PAC configuration enabled: URL={}, AutoDetect={}", pac_url, auto_detect);
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".to_string();
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
})
|
||||
} else {
|
||||
log::debug!(target: "app", "PAC configuration not enabled");
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// macOS: 使用 scutil --proxy 命令
|
||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::debug!(target: "app", "scutil output: {stdout}");
|
||||
|
||||
let mut pac_enabled = false;
|
||||
let mut pac_url = String::new();
|
||||
|
||||
// 解析 scutil 输出
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
|
||||
pac_enabled = true;
|
||||
} else if line.contains("ProxyAutoConfigURLString") {
|
||||
// 正确解析包含冒号的URL
|
||||
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
|
||||
if let Some(colon_pos) = line.find(" : ") {
|
||||
pac_url = line[colon_pos + 3..].trim().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "Parse result: pac_enabled={pac_enabled}, pac_url={pac_url}");
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: pac_enabled && !pac_url.is_empty(),
|
||||
url: pac_url,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// Linux: 检查环境变量和GNOME设置
|
||||
|
||||
// 首先检查环境变量
|
||||
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
|
||||
if !auto_proxy.is_empty() {
|
||||
return Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: auto_proxy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||
let output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(output) = output {
|
||||
if output.status.success() {
|
||||
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if mode.contains("auto") {
|
||||
// 获取 PAC URL
|
||||
let pac_output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(pac_output) = pac_output {
|
||||
if pac_output.status.success() {
|
||||
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
|
||||
if !pac_url.is_empty() {
|
||||
return Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
// Windows: 使用注册表直接读取代理设置
|
||||
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
||||
Self::get_system_proxy_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
||||
|
||||
unsafe {
|
||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||
.encode_utf16()
|
||||
.collect::<Vec<u16>>();
|
||||
|
||||
let mut hkey: HKEY = ptr::null_mut();
|
||||
let result =
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
// 检查代理是否启用
|
||||
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut proxy_enable: DWORD = 0;
|
||||
let mut buffer_size: DWORD = 4;
|
||||
let mut value_type: DWORD = 0;
|
||||
|
||||
let enable_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_enable_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut value_type,
|
||||
&mut proxy_enable as *mut DWORD as *mut u8,
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
|
||||
RegCloseKey(hkey);
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
// 读取代理服务器设置
|
||||
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut buffer = vec![0u16; 1024];
|
||||
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
|
||||
let mut value_type: DWORD = 0;
|
||||
|
||||
let server_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_server_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut value_type,
|
||||
buffer.as_mut_ptr() as *mut u8,
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
let mut proxy_server = String::new();
|
||||
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
|
||||
}
|
||||
|
||||
// 读取代理绕过列表
|
||||
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut bypass_buffer = vec![0u16; 1024];
|
||||
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
|
||||
let mut bypass_value_type: DWORD = 0;
|
||||
|
||||
let override_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_override_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut bypass_value_type,
|
||||
bypass_buffer.as_mut_ptr() as *mut u8,
|
||||
&mut bypass_buffer_size,
|
||||
);
|
||||
|
||||
let mut bypass_list = String::new();
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
|
||||
}
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
if !proxy_server.is_empty() {
|
||||
// 解析服务器地址和端口
|
||||
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
|
||||
let host = proxy_server[..colon_pos].to_string();
|
||||
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "Read proxy settings from registry: {}:{}, bypass: {}", host, port, bypass_list);
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: bypass_list,
|
||||
})
|
||||
} else {
|
||||
Ok(AsyncSysproxy::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::debug!(target: "app", "scutil proxy output: {stdout}");
|
||||
|
||||
let mut http_enabled = false;
|
||||
let mut http_host = String::new();
|
||||
let mut http_port = 8080u16;
|
||||
let mut exceptions = Vec::new();
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.contains("HTTPEnable") && line.contains("1") {
|
||||
http_enabled = true;
|
||||
} else if line.contains("HTTPProxy") && !line.contains("Port") {
|
||||
if let Some(host_part) = line.split(':').nth(1) {
|
||||
http_host = host_part.trim().to_string();
|
||||
}
|
||||
} else if line.contains("HTTPPort") {
|
||||
if let Some(port_part) = line.split(':').nth(1) {
|
||||
if let Ok(port) = port_part.trim().parse::<u16>() {
|
||||
http_port = port;
|
||||
}
|
||||
}
|
||||
} else if line.contains("ExceptionsList") {
|
||||
// 解析异常列表
|
||||
if let Some(list_part) = line.split(':').nth(1) {
|
||||
let list = list_part.trim();
|
||||
if !list.is_empty() {
|
||||
exceptions.push(list.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: http_enabled && !http_host.is_empty(),
|
||||
host: http_host,
|
||||
port: http_port,
|
||||
bypass: exceptions.join(","),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
// Linux: 检查环境变量和桌面环境设置
|
||||
|
||||
// 首先检查环境变量
|
||||
if let Ok(http_proxy) = std::env::var("http_proxy") {
|
||||
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(https_proxy) = std::env::var("https_proxy") {
|
||||
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||
let mode_output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(mode_output) = mode_output {
|
||||
if mode_output.status.success() {
|
||||
let mode = String::from_utf8_lossy(&mode_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
if mode.contains("manual") {
|
||||
// 获取HTTP代理设置
|
||||
let host_result = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy.http", "host"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let port_result = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy.http", "port"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
|
||||
if host_output.status.success() && port_output.status.success() {
|
||||
let host = String::from_utf8_lossy(&host_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
|
||||
let port = String::from_utf8_lossy(&port_output.stdout)
|
||||
.trim()
|
||||
.parse::<u16>()
|
||||
.unwrap_or(8080);
|
||||
|
||||
if !host.is_empty() {
|
||||
return Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy::default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
|
||||
// 解析形如 "http://proxy.example.com:8080" 的URL
|
||||
let url = proxy_url.trim();
|
||||
|
||||
// 移除协议前缀
|
||||
let url = if let Some(stripped) = url.strip_prefix("http://") {
|
||||
stripped
|
||||
} else if let Some(stripped) = url.strip_prefix("https://") {
|
||||
stripped
|
||||
} else {
|
||||
url
|
||||
};
|
||||
|
||||
// 解析主机和端口
|
||||
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
|
||||
let host = url[..colon_pos].to_string();
|
||||
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(url.to_string(), 8080)
|
||||
};
|
||||
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("Invalid proxy URL"));
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: std::env::var("no_proxy").unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -108,14 +108,11 @@ impl WebDavClient {
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.user_agent(format!(
|
||||
"clash-verge/{} ({} WebDAV-Client)",
|
||||
APP_VERSION, OS
|
||||
))
|
||||
.user_agent(format!("koala-clash/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||
// 允许所有请求类型的重定向,包括PUT
|
||||
if attempt.previous().len() >= 5 {
|
||||
attempt.error("重定向次数过多")
|
||||
attempt.error("Too many redirects")
|
||||
} else {
|
||||
attempt.follow()
|
||||
}
|
||||
@@ -177,7 +174,7 @@ impl WebDavClient {
|
||||
}
|
||||
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("Upload failed, retrying once: {}", e);
|
||||
log::warn!("Upload failed, retrying once: {e}");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
timeout(
|
||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||
@@ -237,7 +234,7 @@ impl WebDavClient {
|
||||
|
||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
||||
let zip_file_name = format!("{OS}-backup-{now}.zip");
|
||||
let zip_path = temp_dir().join(&zip_file_name);
|
||||
|
||||
let file = fs::File::create(&zip_path)?;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::core::tray::Tray;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::{
|
||||
@@ -74,7 +72,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法读取文件以检测类型: {}, 错误: {}",
|
||||
"Failed to read file to detect type: {}, error: {}",
|
||||
path,
|
||||
err
|
||||
);
|
||||
@@ -132,7 +130,7 @@ impl CoreManager {
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法确定文件类型,默认当作YAML处理: {}",
|
||||
"Unable to determine file type, defaulting to YAML handling: {}",
|
||||
path
|
||||
);
|
||||
Ok(false)
|
||||
@@ -148,14 +146,19 @@ impl CoreManager {
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
Some("# Koala Clash Runtime"),
|
||||
)?;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
Ok(())
|
||||
}
|
||||
/// 验证运行时配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Generate temporary config file for validation"
|
||||
);
|
||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
self.validate_config_internal(config_path).await
|
||||
@@ -168,13 +171,18 @@ impl CoreManager {
|
||||
) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"App is exiting, skipping validation"
|
||||
);
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
let error_msg = format!("File not found: {}", config_path);
|
||||
let error_msg = format!("File not found: {config_path}");
|
||||
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
@@ -185,7 +193,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"检测到Merge文件,仅进行语法检查: {}",
|
||||
"Detected merge file, performing syntax check only: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_file_syntax(config_path).await;
|
||||
@@ -203,7 +211,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"无法确定文件类型: {}, 错误: {}",
|
||||
"Unable to determine file type: {}, error: {}",
|
||||
config_path,
|
||||
err
|
||||
);
|
||||
@@ -217,7 +225,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"检测到脚本文件,使用JavaScript验证: {}",
|
||||
"Detected script file, validating with JavaScript: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_script_file(config_path).await;
|
||||
@@ -228,7 +236,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"使用Clash内核验证配置文件: {}",
|
||||
"Validating config file with Clash core: {}",
|
||||
config_path
|
||||
);
|
||||
self.validate_config_internal(config_path).await
|
||||
@@ -237,7 +245,12 @@ impl CoreManager {
|
||||
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"App is exiting, skipping validation"
|
||||
);
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
@@ -245,17 +258,23 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"开始验证配置文件: {}",
|
||||
"Starting validation for config file: {}",
|
||||
config_path
|
||||
);
|
||||
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
logging!(info, Type::Config, true, "Using core: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
||||
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Validation directory: {}",
|
||||
app_dir_str
|
||||
);
|
||||
|
||||
// 使用子进程运行clash验证配置
|
||||
let output = app_handle
|
||||
@@ -273,56 +292,84 @@ impl CoreManager {
|
||||
let has_error =
|
||||
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
logging!(info, Type::Config, true, "-------- 验证结果 --------");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"-------- Validation Result --------"
|
||||
);
|
||||
|
||||
if !stderr.is_empty() {
|
||||
logging!(info, Type::Config, true, "stderr输出:\n{}", stderr);
|
||||
logging!(info, Type::Config, true, "stderr output:\n{}", stderr);
|
||||
}
|
||||
|
||||
if has_error {
|
||||
logging!(info, Type::Config, true, "发现错误,开始处理错误信息");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Errors found, processing error details"
|
||||
);
|
||||
let error_msg = if !stdout.is_empty() {
|
||||
stdout.to_string()
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.to_string()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {}", code)
|
||||
format!("Validation process exited abnormally, exit code: {code}")
|
||||
} else {
|
||||
"验证进程被终止".to_string()
|
||||
"Validation process was terminated".to_string()
|
||||
};
|
||||
|
||||
logging!(info, Type::Config, true, "-------- 验证结束 --------");
|
||||
logging!(info, Type::Config, true, "-------- Validation End --------");
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "验证成功");
|
||||
logging!(info, Type::Config, true, "-------- 验证结束 --------");
|
||||
logging!(info, Type::Config, true, "Validation succeeded");
|
||||
logging!(info, Type::Config, true, "-------- Validation End --------");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
}
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Starting file check: {}",
|
||||
config_path
|
||||
);
|
||||
|
||||
// 读取文件内容
|
||||
let content = match std::fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {}", err);
|
||||
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
||||
let error_msg = format!("Failed to read file: {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"Failed to read file: {}",
|
||||
error_msg
|
||||
);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
// 对YAML文件尝试解析,只检查语法正确性
|
||||
logging!(info, Type::Config, true, "进行YAML语法检查");
|
||||
logging!(info, Type::Config, true, "Performing YAML syntax check");
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, true, "YAML语法检查通过");
|
||||
logging!(info, Type::Config, true, "YAML syntax check passed");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {}", err);
|
||||
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
||||
let error_msg = format!("YAML syntax error: {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"YAML syntax error: {}",
|
||||
error_msg
|
||||
);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
}
|
||||
@@ -333,14 +380,20 @@ impl CoreManager {
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {}", err);
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
let error_msg = format!("Failed to read script file: {err}");
|
||||
logging!(warn, Type::Config, true, "Script syntax error: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
|
||||
logging!(debug, Type::Config, true, "验证脚本文件: {}", path);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"Validating script file: {}",
|
||||
path
|
||||
);
|
||||
|
||||
// 使用boa引擎进行基本语法检查
|
||||
use boa_engine::{Context, Source};
|
||||
@@ -350,7 +403,13 @@ impl CoreManager {
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Config,
|
||||
true,
|
||||
"Script syntax validation passed: {}",
|
||||
path
|
||||
);
|
||||
|
||||
// 检查脚本是否包含main函数
|
||||
if !content.contains("function main")
|
||||
@@ -358,7 +417,13 @@ impl CoreManager {
|
||||
&& !content.contains("let main")
|
||||
{
|
||||
let error_msg = "Script must contain a main function";
|
||||
logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"Script missing main function: {}",
|
||||
path
|
||||
);
|
||||
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
|
||||
return Ok((false, error_msg.to_string()));
|
||||
}
|
||||
@@ -366,8 +431,8 @@ impl CoreManager {
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {}", err);
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
let error_msg = format!("Script syntax error: {err}");
|
||||
logging!(warn, Type::Config, true, "Script syntax error: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
@@ -377,33 +442,55 @@ impl CoreManager {
|
||||
pub async fn update_config(&self) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过完整验证流程
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(info, Type::Config, true, "应用正在退出,跳过验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"App is exiting, skipping validation"
|
||||
);
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
logging!(info, Type::Config, true, "开始更新配置");
|
||||
logging!(info, Type::Config, true, "Starting config update");
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
logging!(info, Type::Config, true, "生成新的配置内容");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"Generating new configuration content"
|
||||
);
|
||||
Config::generate().await?;
|
||||
|
||||
// 2. 验证配置
|
||||
match self.validate_config().await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, true, "配置验证通过");
|
||||
logging!(info, Type::Config, true, "Configuration validation passed");
|
||||
// 4. 验证通过后,生成正式的运行时配置
|
||||
logging!(info, Type::Config, true, "生成运行时配置");
|
||||
logging!(info, Type::Config, true, "Generating runtime configuration");
|
||||
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
|
||||
Ok((true, "something".into()))
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"Configuration validation failed: {}",
|
||||
error_msg
|
||||
);
|
||||
Config::runtime().discard();
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"Error occurred during validation: {}",
|
||||
e
|
||||
);
|
||||
Config::runtime().discard();
|
||||
Err(e)
|
||||
}
|
||||
@@ -435,6 +522,322 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
/// 清理多余的 mihomo 进程
|
||||
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Starting cleanup of orphaned mihomo processes"
|
||||
);
|
||||
|
||||
// 获取当前管理的进程 PID
|
||||
let current_pid = {
|
||||
let child_guard = self.child_sidecar.lock().await;
|
||||
child_guard.as_ref().map(|child| child.pid())
|
||||
};
|
||||
|
||||
let target_processes = ["koala-mihomo", "koala-mihomo-alpha"];
|
||||
|
||||
// 并行查找所有目标进程
|
||||
let mut process_futures = Vec::new();
|
||||
for &target in &target_processes {
|
||||
let process_name = if cfg!(windows) {
|
||||
format!("{target}.exe")
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
process_futures.push(self.find_processes_by_name(process_name, target));
|
||||
}
|
||||
|
||||
let process_results = futures::future::join_all(process_futures).await;
|
||||
|
||||
// 收集所有需要终止的进程PID
|
||||
let mut pids_to_kill = Vec::new();
|
||||
for result in process_results {
|
||||
match result {
|
||||
Ok((pids, process_name)) => {
|
||||
for pid in pids {
|
||||
// 跳过当前管理的进程
|
||||
if let Some(current) = current_pid {
|
||||
if pid == current {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"Skipping currently managed process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pids_to_kill.push((pid, process_name.clone()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"Error occurred while finding processes: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pids_to_kill.is_empty() {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"No orphaned mihomo processes found"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut kill_futures = Vec::new();
|
||||
for (pid, process_name) in &pids_to_kill {
|
||||
kill_futures.push(self.kill_process_with_verification(*pid, process_name.clone()));
|
||||
}
|
||||
|
||||
let kill_results = futures::future::join_all(kill_futures).await;
|
||||
|
||||
let killed_count = kill_results.into_iter().filter(|&success| success).count();
|
||||
|
||||
if killed_count > 0 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Cleanup complete, a total of {} redundant mihomo processes terminated",
|
||||
killed_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据进程名查找进程PID列
|
||||
async fn find_processes_by_name(
|
||||
&self,
|
||||
process_name: String,
|
||||
_target: &str,
|
||||
) -> Result<(Vec<u32>, String)> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::mem;
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::tlhelp32::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||
TH32CS_SNAPPROCESS,
|
||||
};
|
||||
use winapi::um::winnt::HANDLE;
|
||||
|
||||
let process_name_clone = process_name.clone();
|
||||
let pids = tokio::task::spawn_blocking(move || -> Result<Vec<u32>> {
|
||||
let mut pids = Vec::new();
|
||||
|
||||
unsafe {
|
||||
// 创建进程快照
|
||||
let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
|
||||
return Err(anyhow::anyhow!("Failed to create process snapshot"));
|
||||
}
|
||||
|
||||
let mut pe32: PROCESSENTRY32W = mem::zeroed();
|
||||
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
|
||||
|
||||
// 获取第一个进程
|
||||
if Process32FirstW(snapshot, &mut pe32) != 0 {
|
||||
loop {
|
||||
// 将宽字符转换为String
|
||||
let end_pos = pe32
|
||||
.szExeFile
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(pe32.szExeFile.len());
|
||||
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
|
||||
|
||||
// 检查进程名是否匹配
|
||||
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
|
||||
pids.push(pe32.th32ProcessID);
|
||||
}
|
||||
if Process32NextW(snapshot, &mut pe32) == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭句柄
|
||||
CloseHandle(snapshot);
|
||||
}
|
||||
|
||||
Ok(pids)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok((pids, process_name))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let output = if cfg!(target_os = "macos") {
|
||||
tokio::process::Command::new("pgrep")
|
||||
.arg(&process_name)
|
||||
.output()
|
||||
.await?
|
||||
} else {
|
||||
// Linux
|
||||
tokio::process::Command::new("pidof")
|
||||
.arg(&process_name)
|
||||
.output()
|
||||
.await?
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok((Vec::new(), process_name));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut pids = Vec::new();
|
||||
|
||||
// Unix系统直接解析PID列表
|
||||
for pid_str in stdout.split_whitespace() {
|
||||
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||
pids.push(pid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((pids, process_name))
|
||||
}
|
||||
}
|
||||
|
||||
/// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效
|
||||
async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Attempt to terminate process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
|
||||
#[cfg(windows)]
|
||||
let success = {
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
|
||||
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
|
||||
|
||||
tokio::task::spawn_blocking(move || -> bool {
|
||||
unsafe {
|
||||
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
||||
if process_handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
let result = TerminateProcess(process_handle, 1);
|
||||
CloseHandle(process_handle);
|
||||
|
||||
result != 0
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let success = {
|
||||
tokio::process::Command::new("kill")
|
||||
.args(["-9", &pid.to_string()])
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if success {
|
||||
// 短暂等待并验证进程是否真正终止
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
let still_running = self.is_process_running(pid).await.unwrap_or(false);
|
||||
if still_running {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"Process {} (PID: {}) Termination command successful, but process still running",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Successfully terminated process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
true
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"Unable to terminate process: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows API检查进程
|
||||
async fn is_process_running(&self, pid: u32) -> Result<bool> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winapi::shared::minwindef::DWORD;
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::GetExitCodeProcess;
|
||||
use winapi::um::processthreadsapi::OpenProcess;
|
||||
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<bool> {
|
||||
unsafe {
|
||||
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
|
||||
if process_handle.is_null() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut exit_code: DWORD = 0;
|
||||
let result = GetExitCodeProcess(process_handle, &mut exit_code);
|
||||
CloseHandle(process_handle);
|
||||
|
||||
if result == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(exit_code == 259)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let output = tokio::process::Command::new("ps")
|
||||
.args(["-p", &pid.to_string()])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
Ok(output.status.success() && !output.stdout.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Running core by sidecar");
|
||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||
@@ -450,7 +853,7 @@ impl CoreManager {
|
||||
let now = Local::now();
|
||||
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||
|
||||
let log_path = service_log_dir.join(format!("sidecar_{}.log", timestamp));
|
||||
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
|
||||
|
||||
let mut log_file = File::create(log_path)?;
|
||||
|
||||
@@ -539,19 +942,29 @@ impl CoreManager {
|
||||
// 当服务安装失败时的回退逻辑
|
||||
async fn attempt_service_init(&self) -> Result<()> {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service version mismatch or abnormal status, performing reinstallation"
|
||||
);
|
||||
if let Err(e) = service::reinstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务重装失败 during attempt_service_init: {}",
|
||||
"Service reinstallation failed during attempt_service_init: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
// 如果重装成功,还需要尝试启动服务
|
||||
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service reinstalled successfully, attempting to start"
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = self.start_core_by_service().await {
|
||||
@@ -559,20 +972,20 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"通过服务启动核心失败 during attempt_service_init: {}",
|
||||
"Failed to start core via service during attempt_service_init: {}",
|
||||
e
|
||||
);
|
||||
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
|
||||
let mut state = service::ServiceState::get();
|
||||
if !state.prefer_sidecar {
|
||||
state.prefer_sidecar = true;
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {}", e));
|
||||
state.last_error = Some(format!("Failed to start core via service: {e}"));
|
||||
if let Err(save_err) = state.save() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Core,
|
||||
true,
|
||||
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
|
||||
"Failed to save ServiceState (in attempt_service_init/start_core_by_service): {}",
|
||||
save_err
|
||||
);
|
||||
}
|
||||
@@ -585,6 +998,17 @@ impl CoreManager {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, "Initializing core");
|
||||
|
||||
// 应用启动时先清理任何遗留的 mihomo 进程
|
||||
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"Failed to clean up unnecessary mihomo processes during application initialization: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
let mut core_started_successfully = false;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
@@ -592,11 +1016,16 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
|
||||
"Service currently available or appears available; attempting to start/reinstall via service mode"
|
||||
);
|
||||
match self.attempt_service_init().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, true, "服务模式成功启动核心");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service mode successfully started core"
|
||||
);
|
||||
core_started_successfully = true;
|
||||
}
|
||||
Err(_err) => {
|
||||
@@ -604,7 +1033,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
|
||||
"Service mode start or reinstall failed. Will attempt Sidecar fallback."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -613,7 +1042,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务初始不可用 (is_service_available 调用失败)"
|
||||
"Service initially unavailable (is_service_available call failed)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -622,7 +1051,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
|
||||
"Core not started via service mode; performing Sidecar fallback or first-time install logic"
|
||||
);
|
||||
|
||||
let service_state = service::ServiceState::get();
|
||||
@@ -632,7 +1061,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
|
||||
"User prefers Sidecar mode or previous service start failed; starting with Sidecar mode"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
|
||||
@@ -644,26 +1073,41 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
|
||||
"No service installation record (first run or state reset); attempting to install service"
|
||||
);
|
||||
match service::install_service().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service installed successfully (first attempt)"
|
||||
);
|
||||
let mut new_state = service::ServiceState::default();
|
||||
new_state.record_install();
|
||||
new_state.prefer_sidecar = false;
|
||||
new_state.save()?;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Newly installed service available; attempting to start"
|
||||
);
|
||||
if self.start_core_by_service().await.is_ok() {
|
||||
logging!(info, Type::Core, true, "新安装的服务启动成功");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Newly installed service started successfully"
|
||||
);
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"新安装的服务启动失败,回退到Sidecar模式"
|
||||
"Newly installed service failed to start; falling back to Sidecar mode"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
final_state.prefer_sidecar = true;
|
||||
@@ -677,7 +1121,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
|
||||
"Service installed successfully but not connectable/immediately available; falling back to Sidecar mode"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
final_state.prefer_sidecar = true;
|
||||
@@ -690,7 +1134,13 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service first-time installation failed: {}",
|
||||
err
|
||||
);
|
||||
let new_state = service::ServiceState {
|
||||
last_error: Some(err.to_string()),
|
||||
prefer_sidecar: true,
|
||||
@@ -708,7 +1158,7 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
|
||||
"There is a service installation record, but the service is unavailable/not started. Force switch to Sidecar mode"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
if !final_state.prefer_sidecar {
|
||||
@@ -716,7 +1166,7 @@ impl CoreManager {
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true"
|
||||
"prefer_sidecar is false, but is forced to true due to service startup failure or unavailability"
|
||||
);
|
||||
final_state.prefer_sidecar = true;
|
||||
final_state.last_error =
|
||||
@@ -732,9 +1182,8 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
logging!(trace, Type::Core, "Initied core logic completed");
|
||||
#[cfg(target_os = "macos")]
|
||||
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -754,7 +1203,12 @@ impl CoreManager {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
service::reinstall_service().await?;
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service available; starting in service mode"
|
||||
);
|
||||
self.start_core_by_service().await?;
|
||||
} else {
|
||||
// 服务不可用,检查用户偏好
|
||||
@@ -764,11 +1218,16 @@ impl CoreManager {
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
"Service unavailable; starting in Sidecar mode per user preference"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
} else {
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"Service unavailable; starting in Sidecar mode"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
@@ -787,6 +1246,7 @@ impl CoreManager {
|
||||
/// 重启内核
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
self.stop_core().await?;
|
||||
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -800,7 +1260,7 @@ impl CoreManager {
|
||||
}
|
||||
let core: &str = &clash_core.clone().unwrap();
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&core) {
|
||||
let error_message = format!("Clash core invalid name: {}", core);
|
||||
let error_message = format!("Clash core invalid name: {core}");
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message);
|
||||
}
|
||||
|
||||
571
src-tauri/src/core/event_driven_proxy.rs
Normal file
@@ -0,0 +1,571 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
|
||||
use crate::config::{Config, IVerge};
|
||||
use crate::core::async_proxy_query::AsyncProxyQuery;
|
||||
use crate::logging_error;
|
||||
use crate::utils::logging::Type;
|
||||
use once_cell::sync::Lazy;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProxyEvent {
|
||||
/// 配置变更事件
|
||||
ConfigChanged,
|
||||
/// 强制检查代理状态
|
||||
#[allow(dead_code)]
|
||||
ForceCheck,
|
||||
/// 启用系统代理
|
||||
#[allow(dead_code)]
|
||||
EnableProxy,
|
||||
/// 禁用系统代理
|
||||
#[allow(dead_code)]
|
||||
DisableProxy,
|
||||
/// 切换到PAC模式
|
||||
#[allow(dead_code)]
|
||||
SwitchToPac,
|
||||
/// 切换到HTTP代理模式
|
||||
#[allow(dead_code)]
|
||||
SwitchToHttp,
|
||||
/// 应用启动事件
|
||||
AppStarted,
|
||||
/// 应用关闭事件
|
||||
#[allow(dead_code)]
|
||||
AppStopping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyState {
|
||||
pub sys_enabled: bool,
|
||||
pub pac_enabled: bool,
|
||||
pub auto_proxy: Autoproxy,
|
||||
pub sys_proxy: Sysproxy,
|
||||
pub last_updated: std::time::Instant,
|
||||
pub is_healthy: bool,
|
||||
}
|
||||
|
||||
impl Default for ProxyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sys_enabled: false,
|
||||
pac_enabled: false,
|
||||
auto_proxy: Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
},
|
||||
sys_proxy: Sysproxy {
|
||||
enable: false,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 7890,
|
||||
bypass: "".to_string(),
|
||||
},
|
||||
last_updated: std::time::Instant::now(),
|
||||
is_healthy: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventDrivenProxyManager {
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
event_sender: mpsc::UnboundedSender<ProxyEvent>,
|
||||
query_sender: mpsc::UnboundedSender<QueryRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QueryRequest {
|
||||
response_tx: oneshot::Sender<Autoproxy>,
|
||||
}
|
||||
|
||||
// Configuration structure moved to external
|
||||
struct ProxyConfig {
|
||||
sys_enabled: bool,
|
||||
pac_enabled: bool,
|
||||
guard_enabled: bool,
|
||||
}
|
||||
|
||||
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
||||
|
||||
impl EventDrivenProxyManager {
|
||||
pub fn global() -> &'static EventDrivenProxyManager {
|
||||
&PROXY_MANAGER
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
let state = Arc::new(RwLock::new(ProxyState::default()));
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
||||
|
||||
Self::start_event_loop(state.clone(), event_rx, query_rx);
|
||||
|
||||
Self {
|
||||
state,
|
||||
event_sender: event_tx,
|
||||
query_sender: query_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get automatic proxy configuration (cached)
|
||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||
self.state.read().auto_proxy.clone()
|
||||
}
|
||||
|
||||
/// Asynchronously get the latest automatic proxy configuration
|
||||
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let query = QueryRequest { response_tx: tx };
|
||||
|
||||
if self.query_sender.send(query).is_err() {
|
||||
log::error!(target: "app", "Failed to send query request, returning cached data");
|
||||
return self.get_auto_proxy_cached();
|
||||
}
|
||||
|
||||
match timeout(Duration::from_secs(5), rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
_ => {
|
||||
log::warn!(target: "app", "Query timed out, returning cached data");
|
||||
self.get_auto_proxy_cached()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify configuration changed
|
||||
pub fn notify_config_changed(&self) {
|
||||
self.send_event(ProxyEvent::ConfigChanged);
|
||||
}
|
||||
|
||||
/// Notify application started
|
||||
pub fn notify_app_started(&self) {
|
||||
self.send_event(ProxyEvent::AppStarted);
|
||||
}
|
||||
|
||||
/// Notify application stopping
|
||||
#[allow(dead_code)]
|
||||
pub fn notify_app_stopping(&self) {
|
||||
self.send_event(ProxyEvent::AppStopping);
|
||||
}
|
||||
|
||||
/// Enable system proxy
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::EnableProxy);
|
||||
}
|
||||
|
||||
/// Disable system proxy
|
||||
#[allow(dead_code)]
|
||||
pub fn disable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::DisableProxy);
|
||||
}
|
||||
|
||||
/// Force check proxy status
|
||||
#[allow(dead_code)]
|
||||
pub fn force_check(&self) {
|
||||
self.send_event(ProxyEvent::ForceCheck);
|
||||
}
|
||||
|
||||
fn send_event(&self, event: ProxyEvent) {
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
log::error!(target: "app", "Failed to send proxy event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn start_event_loop(
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
log::info!(target: "app", "Event-driven proxy manager started");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = event_rx.recv() => {
|
||||
match event {
|
||||
Some(event) => {
|
||||
log::debug!(target: "app", "Handling proxy event: {event:?}");
|
||||
Self::handle_event(&state, event).await;
|
||||
}
|
||||
None => {
|
||||
log::info!(target: "app", "Event channel closed, proxy manager stopped");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
query = query_rx.recv() => {
|
||||
match query {
|
||||
Some(query) => {
|
||||
let result = Self::handle_query(&state).await;
|
||||
let _ = query.response_tx.send(result);
|
||||
}
|
||||
None => {
|
||||
log::info!(target: "app", "Query channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
||||
match event {
|
||||
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
|
||||
Self::update_proxy_config(state).await;
|
||||
}
|
||||
ProxyEvent::EnableProxy => {
|
||||
Self::enable_system_proxy(state).await;
|
||||
}
|
||||
ProxyEvent::DisableProxy => {
|
||||
Self::disable_system_proxy(state).await;
|
||||
}
|
||||
ProxyEvent::SwitchToPac => {
|
||||
Self::switch_proxy_mode(state, true).await;
|
||||
}
|
||||
ProxyEvent::SwitchToHttp => {
|
||||
Self::switch_proxy_mode(state, false).await;
|
||||
}
|
||||
ProxyEvent::AppStarted => {
|
||||
Self::initialize_proxy_state(state).await;
|
||||
}
|
||||
ProxyEvent::AppStopping => {
|
||||
log::info!(target: "app", "Cleaning up proxy state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = auto_proxy.clone();
|
||||
});
|
||||
|
||||
auto_proxy
|
||||
}
|
||||
|
||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "Initializing proxy state");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
s.auto_proxy = auto_proxy;
|
||||
s.sys_proxy = sys_proxy;
|
||||
s.is_healthy = true;
|
||||
});
|
||||
|
||||
log::info!(target: "app", "Proxy state initialized: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
}
|
||||
|
||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::debug!(target: "app", "Updating proxy configuration");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
});
|
||||
|
||||
if config.guard_enabled && config.sys_enabled {
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let (sys_enabled, pac_enabled) = {
|
||||
let s = state.read();
|
||||
(s.sys_enabled, s.pac_enabled)
|
||||
};
|
||||
|
||||
if !sys_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "Checking proxy status");
|
||||
|
||||
if pac_enabled {
|
||||
Self::check_and_restore_pac_proxy(state).await;
|
||||
} else {
|
||||
Self::check_and_restore_sys_proxy(state).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let current = Self::get_auto_proxy_with_timeout().await;
|
||||
let expected = Self::get_expected_pac_config();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = current.clone();
|
||||
});
|
||||
|
||||
if !current.enable || current.url != expected.url {
|
||||
log::info!(target: "app", "PAC proxy setting abnormal, recovering...");
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let restored = Self::get_auto_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable && restored.url == expected.url;
|
||||
s.auto_proxy = restored;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let current = Self::get_sys_proxy_with_timeout().await;
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_proxy = current.clone();
|
||||
});
|
||||
|
||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||
log::info!(target: "app", "System proxy setting abnormal, recovering...");
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let restored = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable
|
||||
&& restored.host == expected.host
|
||||
&& restored.port == expected.port;
|
||||
s.sys_proxy = restored;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "Enabling system proxy");
|
||||
|
||||
let pac_enabled = state.read().pac_enabled;
|
||||
|
||||
if pac_enabled {
|
||||
let expected = Self::get_expected_pac_config();
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
} else {
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
}
|
||||
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
|
||||
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "Disabling system proxy");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let disabled_sys = Sysproxy::default();
|
||||
let disabled_auto = Autoproxy::default();
|
||||
|
||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||
}
|
||||
}
|
||||
|
||||
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
|
||||
log::info!(target: "app", "Switching to {} mode", if to_pac { "PAC" } else { "HTTP Proxy" });
|
||||
|
||||
if to_pac {
|
||||
let disabled_sys = Sysproxy::default();
|
||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||
|
||||
let expected = Self::get_expected_pac_config();
|
||||
Self::restore_pac_proxy(&expected.url).await;
|
||||
} else {
|
||||
let disabled_auto = Autoproxy::default();
|
||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
Self::restore_sys_proxy(&expected).await;
|
||||
}
|
||||
|
||||
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
|
||||
async fn get_auto_proxy_with_timeout() -> Autoproxy {
|
||||
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
|
||||
|
||||
// Convert to compatible structure
|
||||
Autoproxy {
|
||||
enable: async_proxy.enable,
|
||||
url: async_proxy.url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_sys_proxy_with_timeout() -> Sysproxy {
|
||||
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
// Convert to compatible structure
|
||||
Sysproxy {
|
||||
enable: async_proxy.enable,
|
||||
host: async_proxy.host,
|
||||
port: async_proxy.port,
|
||||
bypass: async_proxy.bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// Unified state update method
|
||||
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
||||
where
|
||||
F: FnOnce(&mut ProxyState),
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
update_fn(&mut state_guard);
|
||||
state_guard.last_updated = std::time::Instant::now();
|
||||
}
|
||||
|
||||
fn get_proxy_config() -> ProxyConfig {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest();
|
||||
ProxyConfig {
|
||||
sys_enabled: verge.enable_system_proxy.unwrap_or(false),
|
||||
pac_enabled: verge.proxy_auto_config.unwrap_or(false),
|
||||
guard_enabled: verge.enable_proxy_guard.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_pac_config() -> Autoproxy {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest();
|
||||
let (proxy_host, pac_port) = (
|
||||
verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||
IVerge::get_singleton_port(),
|
||||
);
|
||||
Autoproxy {
|
||||
enable: true,
|
||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_sys_proxy() -> Sysproxy {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest();
|
||||
let port = verge
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let proxy_host = verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
|
||||
Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host,
|
||||
port,
|
||||
bypass: Self::get_bypass_config(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bypass_config() -> String {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest();
|
||||
let use_default = verge.use_default_bypass.unwrap_or(true);
|
||||
let custom_bypass = verge.system_proxy_bypass.clone().unwrap_or_default();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let default_bypass = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let default_bypass =
|
||||
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let default_bypass = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
if custom_bypass.is_empty() {
|
||||
default_bypass.to_string()
|
||||
} else if use_default {
|
||||
format!("{default_bypass},{custom_bypass}")
|
||||
} else {
|
||||
custom_bypass
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_pac_proxy(expected_url: &str) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_autoproxy = Autoproxy {
|
||||
enable: true,
|
||||
url: expected_url.to_string(),
|
||||
};
|
||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Self::execute_sysproxy_command(&["pac", expected_url]).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_sys_proxy(expected: &Sysproxy) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
logging_error!(Type::System, true, expected.set_system_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let address = format!("{}:{}", expected.host, expected.port);
|
||||
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn execute_sysproxy_command(args: &[&str]) {
|
||||
use crate::utils::dirs;
|
||||
#[allow(unused_imports)] // creation_flags必须
|
||||
use std::os::windows::process::CommandExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get service path: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new(sysproxy_exe)
|
||||
.args(args)
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "Failed to execute sysproxy command: {:?}", args);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.is_empty() {
|
||||
log::error!(target: "app", "sysproxy stderr: {}", stderr);
|
||||
}
|
||||
} else {
|
||||
log::debug!(target: "app", "Successfully executed sysproxy command: {:?}", args);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Error executing sysproxy command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl NotificationSystem {
|
||||
match serde_json::to_value((status, message)) {
|
||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||
Err(e) => {
|
||||
log::error!("Failed to serialize NoticeMessage payload: {}", e);
|
||||
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
||||
("verge://notice-message", Err(e))
|
||||
}
|
||||
}
|
||||
@@ -153,11 +153,11 @@ impl NotificationSystem {
|
||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||
// 记录成功发送的事件
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
log::debug!("Successfully emitted event: {}", event_name_str);
|
||||
log::debug!("Successfully emitted event: {event_name_str}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
||||
log::warn!("Failed to emit event {event_name_str}: {e}");
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
|
||||
@@ -165,8 +165,7 @@ impl NotificationSystem {
|
||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||
log::warn!(
|
||||
"Reached {} emit errors, entering emergency mode",
|
||||
EMIT_ERROR_THRESHOLD
|
||||
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
||||
);
|
||||
*system.emergency_mode.write() = true;
|
||||
}
|
||||
@@ -175,7 +174,7 @@ impl NotificationSystem {
|
||||
} else {
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
|
||||
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window found, skipping event emit.");
|
||||
@@ -215,7 +214,7 @@ impl NotificationSystem {
|
||||
match sender.send(event) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to send event to notification queue: {:?}", e);
|
||||
log::warn!("Failed to send event to notification queue: {e:?}");
|
||||
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||
false
|
||||
@@ -243,7 +242,7 @@ impl NotificationSystem {
|
||||
log::info!("NotificationSystem worker thread joined successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("NotificationSystem worker thread join failed: {:?}", e);
|
||||
log::error!("NotificationSystem worker thread join failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,6 +258,8 @@ pub struct Handle {
|
||||
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
startup_completed: Arc<RwLock<bool>>,
|
||||
notification_system: Arc<RwLock<Option<NotificationSystem>>>,
|
||||
/// Messages that should be emitted only after UI is really ready
|
||||
ui_pending_messages: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||
}
|
||||
|
||||
impl Default for Handle {
|
||||
@@ -269,6 +270,7 @@ impl Default for Handle {
|
||||
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||
startup_completed: Arc::new(RwLock::new(false)),
|
||||
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
|
||||
ui_pending_messages: Arc::new(RwLock::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +298,10 @@ impl Handle {
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Option<WebviewWindow> {
|
||||
// If we are in lightweight mode, treat as no window (webview may be destroyed)
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
return None;
|
||||
}
|
||||
let app_handle = self.app_handle()?;
|
||||
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
||||
if window.is_none() {
|
||||
@@ -412,12 +418,13 @@ impl Handle {
|
||||
let status_str = status.into();
|
||||
let msg_str = msg.into();
|
||||
|
||||
// If startup not completed, buffer messages (existing behavior)
|
||||
if !*handle.startup_completed.read() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Frontend,
|
||||
true,
|
||||
"启动过程中发现错误,加入消息队列: {} - {}",
|
||||
"Error found during startup; queued: {} - {}",
|
||||
status_str,
|
||||
msg_str
|
||||
);
|
||||
@@ -430,6 +437,23 @@ impl Handle {
|
||||
return;
|
||||
}
|
||||
|
||||
// If UI is not yet ready (e.g., window re-created from tray or lightweight mode),
|
||||
// buffer messages to emit after UI signals readiness.
|
||||
if !crate::utils::resolve::is_ui_ready() {
|
||||
log::debug!(
|
||||
target: "app",
|
||||
"UI not ready, queue notice message: {} - {}",
|
||||
status_str,
|
||||
msg_str
|
||||
);
|
||||
let mut pendings = handle.ui_pending_messages.write();
|
||||
pendings.push(ErrorMessage {
|
||||
status: status_str,
|
||||
message: msg_str,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if handle.is_exiting() {
|
||||
return;
|
||||
}
|
||||
@@ -443,6 +467,34 @@ impl Handle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush messages buffered while UI was not ready
|
||||
pub fn flush_ui_pending_messages(&self) {
|
||||
let pending = {
|
||||
let mut msgs = self.ui_pending_messages.write();
|
||||
std::mem::take(&mut *msgs)
|
||||
};
|
||||
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
let system_opt = self.notification_system.read();
|
||||
if let Some(system) = system_opt.as_ref() {
|
||||
for msg in pending {
|
||||
system.send_event(FrontendEvent::NoticeMessage {
|
||||
status: msg.status,
|
||||
message: msg.message,
|
||||
});
|
||||
// small pacing to avoid flooding immediately on resume
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_startup_completed(&self) {
|
||||
{
|
||||
let mut completed = self.startup_completed.write();
|
||||
@@ -467,7 +519,7 @@ impl Handle {
|
||||
info,
|
||||
Type::Frontend,
|
||||
true,
|
||||
"发送{}条启动时累积的错误消息",
|
||||
"Sending {} accumulated startup error messages",
|
||||
errors.len()
|
||||
);
|
||||
|
||||
@@ -500,7 +552,7 @@ impl Handle {
|
||||
});
|
||||
|
||||
if let Err(e) = thread_result {
|
||||
log::error!("Failed to spawn startup errors thread: {}", e);
|
||||
log::error!("Failed to spawn startup errors thread: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use crate::utils::notification::{notify_event, NotificationEvent};
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::handle,
|
||||
feat, logging, logging_error,
|
||||
module::lightweight::entry_lightweight_mode,
|
||||
process::AsyncHandler,
|
||||
utils::{logging::Type, resolve},
|
||||
config::Config, core::handle, feat, logging, logging_error,
|
||||
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -14,7 +11,7 @@ use tauri::Manager;
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
current: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Hotkey {
|
||||
@@ -38,7 +35,6 @@ impl Hotkey {
|
||||
enable_global_hotkey
|
||||
);
|
||||
|
||||
// 如果全局热键被禁用,则不注册热键
|
||||
if !enable_global_hotkey {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -138,14 +134,11 @@ impl Hotkey {
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
|
||||
"open_or_close_dashboard" => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Registering open_or_close_dashboard function"
|
||||
);
|
||||
|| {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
@@ -153,94 +146,89 @@ impl Hotkey {
|
||||
"=== Hotkey Dashboard Window Operation Start ==="
|
||||
);
|
||||
|
||||
// 检查是否在轻量模式下,如果是,需要同步处理
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"In lightweight mode, calling open_or_close_dashboard directly"
|
||||
);
|
||||
crate::feat::open_or_close_dashboard();
|
||||
} else {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Toggle dashboard window visibility (async)"
|
||||
);
|
||||
logging!(
|
||||
info,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Using unified WindowManager for hotkey operation (bypass debounce)"
|
||||
);
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
if visible {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is visible, hiding it"
|
||||
);
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window is hidden, showing it"
|
||||
);
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"Failed to check window visibility: {}",
|
||||
e
|
||||
);
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"Window does not exist, creating a new one"
|
||||
);
|
||||
resolve::create_window(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
crate::feat::open_or_close_dashboard_hotkey();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"=== Hotkey Dashboard Window Operation End ==="
|
||||
);
|
||||
}
|
||||
notify_event(&app_handle, NotificationEvent::DashboardToggled);
|
||||
})
|
||||
}
|
||||
"clash_mode_rule" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::change_clash_mode("rule".into());
|
||||
notify_event(
|
||||
&app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||
);
|
||||
})
|
||||
}
|
||||
"clash_mode_global" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::change_clash_mode("global".into());
|
||||
notify_event(
|
||||
&app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||
);
|
||||
})
|
||||
}
|
||||
"clash_mode_direct" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::change_clash_mode("direct".into());
|
||||
notify_event(
|
||||
&app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||
);
|
||||
})
|
||||
}
|
||||
"toggle_system_proxy" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::toggle_system_proxy();
|
||||
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
|
||||
})
|
||||
}
|
||||
"toggle_tun_mode" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::toggle_tun_mode(None);
|
||||
notify_event(&app_handle, NotificationEvent::TunModeToggled);
|
||||
})
|
||||
}
|
||||
"entry_lightweight_mode" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
entry_lightweight_mode();
|
||||
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
|
||||
})
|
||||
}
|
||||
"quit" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::quit();
|
||||
notify_event(&app_handle, NotificationEvent::AppQuit);
|
||||
})
|
||||
}
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||
"quit" => || feat::quit(),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => || feat::hide(),
|
||||
|
||||
"hide" => {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
Box::new(move || {
|
||||
feat::hide();
|
||||
notify_event(&app_handle, NotificationEvent::AppHidden);
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
||||
bail!("invalid function \"{func}\"");
|
||||
@@ -261,10 +249,8 @@ impl Hotkey {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
|
||||
// 获取全局热键状态
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
@@ -274,7 +260,6 @@ impl Hotkey {
|
||||
f();
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod async_proxy_query;
|
||||
pub mod backup;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod core;
|
||||
pub mod event_driven_proxy;
|
||||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod service;
|
||||
@@ -10,4 +12,4 @@ pub mod timer;
|
||||
pub mod tray;
|
||||
pub mod win_uwp;
|
||||
|
||||
pub use self::{core::*, timer::Timer};
|
||||
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
|
||||
|
||||
@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -466,7 +466,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -477,7 +477,12 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
|
||||
/// 检查服务状态 - 使用IPC通信
|
||||
pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
logging!(info, Type::Service, true, "开始检查服务状态 (IPC)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Starting service status check (IPC)"
|
||||
);
|
||||
|
||||
// 使用IPC通信
|
||||
let payload = serde_json::json!({});
|
||||
@@ -495,8 +500,16 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
); */
|
||||
|
||||
if !response.success {
|
||||
let err_msg = response.error.unwrap_or_else(|| "未知服务错误".to_string());
|
||||
logging!(error, Type::Service, true, "服务响应错误: {}", err_msg);
|
||||
let err_msg = response
|
||||
.error
|
||||
.unwrap_or_else(|| "Unknown service error".to_string());
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service response error: {}",
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
@@ -516,7 +529,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"解析嵌套的ResponseBody失败: {}; 尝试其他方式",
|
||||
"Failed to parse nested ResponseBody: {}; trying alternative",
|
||||
e
|
||||
);
|
||||
None
|
||||
@@ -536,7 +549,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"服务检测成功: code={}, msg={}, data存在={}",
|
||||
"Service check succeeded: code={}, msg={}, data_present={}",
|
||||
json_response.code,
|
||||
json_response.msg,
|
||||
json_response.data.is_some()
|
||||
@@ -550,7 +563,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"服务检测成功: code={}, msg={}",
|
||||
"Service check succeeded: code={}, msg={}",
|
||||
json_response.code,
|
||||
json_response.msg
|
||||
);
|
||||
@@ -561,31 +574,42 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"解析服务响应失败: {}; 原始数据: {:?}",
|
||||
"Failed to parse service response: {}; raw data: {:?}",
|
||||
e,
|
||||
data
|
||||
);
|
||||
bail!("无法解析服务响应数据: {}", e)
|
||||
bail!("Unable to parse service response data: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
logging!(error, Type::Service, true, "服务响应中没有数据");
|
||||
bail!("服务响应中没有数据")
|
||||
logging!(error, Type::Service, true, "No data in service response");
|
||||
bail!("No data in service response")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "IPC通信失败: {}", e);
|
||||
bail!("无法连接到Clash Verge Service: {}", e)
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC communication failed: {}",
|
||||
e
|
||||
);
|
||||
bail!("Unable to connect to Koala Clash Service: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查服务版本 - 使用IPC通信
|
||||
pub async fn check_service_version() -> Result<String> {
|
||||
logging!(info, Type::Service, true, "开始检查服务版本 (IPC)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Starting service version check (IPC)"
|
||||
);
|
||||
|
||||
let payload = serde_json::json!({});
|
||||
// logging!(debug, Type::Service, true, "发送GetVersion请求");
|
||||
@@ -604,8 +628,14 @@ pub async fn check_service_version() -> Result<String> {
|
||||
if !response.success {
|
||||
let err_msg = response
|
||||
.error
|
||||
.unwrap_or_else(|| "获取服务版本失败".to_string());
|
||||
logging!(error, Type::Service, true, "获取版本错误: {}", err_msg);
|
||||
.unwrap_or_else(|| "Failed to get service version".to_string());
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to get service version: {}",
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
@@ -618,7 +648,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"获取到服务版本: {}",
|
||||
"Service version: {}",
|
||||
version_str
|
||||
);
|
||||
return Ok(version_str.to_string());
|
||||
@@ -628,7 +658,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"嵌套数据中没有version字段: {:?}",
|
||||
"Nested data does not contain version field: {:?}",
|
||||
nested_data
|
||||
);
|
||||
} else {
|
||||
@@ -639,7 +669,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"获取到服务版本: {}",
|
||||
"Received service version: {}",
|
||||
version_response.version
|
||||
);
|
||||
return Ok(version_response.version);
|
||||
@@ -649,44 +679,55 @@ pub async fn check_service_version() -> Result<String> {
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"解析版本响应失败: {}; 原始数据: {:?}",
|
||||
"Failed to parse version response: {}; raw data: {:?}",
|
||||
e,
|
||||
data
|
||||
);
|
||||
bail!("无法解析服务版本数据: {}", e)
|
||||
bail!("Unable to parse service version data: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("响应中未找到有效的版本信息")
|
||||
bail!("No valid version information found in response")
|
||||
}
|
||||
None => {
|
||||
logging!(error, Type::Service, true, "版本响应中没有数据");
|
||||
bail!("服务版本响应中没有数据")
|
||||
logging!(error, Type::Service, true, "No data in version response");
|
||||
bail!("No data in service version response")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "IPC通信失败: {}", e);
|
||||
bail!("无法连接到Clash Verge Service: {}", e)
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC communication failed: {}",
|
||||
e
|
||||
);
|
||||
bail!("Unable to connect to Koala Clash Service: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查服务是否需要重装
|
||||
pub async fn check_service_needs_reinstall() -> bool {
|
||||
logging!(info, Type::Service, true, "开始检查服务是否需要重装");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Checking whether service needs reinstallation"
|
||||
);
|
||||
|
||||
let service_state = ServiceState::get();
|
||||
|
||||
if !service_state.can_reinstall() {
|
||||
log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数");
|
||||
log::info!(target: "app", "Service reinstall check: in cooldown period or max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查版本和可用性
|
||||
match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
||||
log::info!(target: "app", "Service version check: current={version}, required={REQUIRED_SERVICE_VERSION}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -698,26 +739,36 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
|
||||
log::warn!(target: "app", "Service version mismatch detected, reinstallation required! current={version}, required={REQUIRED_SERVICE_VERSION}");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service version mismatch, reinstallation required"
|
||||
);
|
||||
|
||||
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
// log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配,无需重装");
|
||||
log::info!(target: "app", "Service version matches, no reinstallation needed");
|
||||
// logging!(info, Type::Service, true, "服务版本匹配,无需重装");
|
||||
}
|
||||
|
||||
needs_reinstall
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Service, true, "检查服务版本失败: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to check service version: {}",
|
||||
err
|
||||
);
|
||||
|
||||
// 检查服务是否可用
|
||||
match is_service_available().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err);
|
||||
log::info!(target: "app", "Service is running but version check failed: {err}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -728,7 +779,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
log::info!(target: "app", "服务不可用或未运行,需要重装");
|
||||
log::info!(target: "app", "Service unavailable or not running, reinstallation needed");
|
||||
// logging!(info, Type::Service, true, "服务不可用或未运行,需要重装");
|
||||
true
|
||||
}
|
||||
@@ -739,7 +790,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
|
||||
/// 尝试使用服务启动core
|
||||
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
|
||||
log::info!(target:"app", "Attempting to start core with existing service (IPC)");
|
||||
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
|
||||
|
||||
let clash_core = Config::verge().latest().get_valid_clash_core();
|
||||
@@ -782,8 +833,16 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
); */
|
||||
|
||||
if !response.success {
|
||||
let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string());
|
||||
logging!(error, Type::Service, true, "启动核心失败: {}", err_msg);
|
||||
let err_msg = response
|
||||
.error
|
||||
.unwrap_or_else(|| "Failed to start core".to_string());
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to start core: {}",
|
||||
err_msg
|
||||
);
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
@@ -794,128 +853,140 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
let msg = data
|
||||
.get("msg")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
.unwrap_or("Unknown error");
|
||||
|
||||
if code_value != 0 {
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"启动核心返回错误: code={}, msg={}",
|
||||
"Start core returned error: code={}, msg={}",
|
||||
code_value,
|
||||
msg
|
||||
);
|
||||
bail!("启动核心失败: {}", msg);
|
||||
bail!("Failed to start core: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logging!(info, Type::Service, true, "服务成功启动核心");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service successfully started core"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e);
|
||||
bail!("无法连接到Clash Verge Service: {}", e)
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to start core via IPC: {}",
|
||||
e
|
||||
);
|
||||
bail!("Unable to connect to Koala Clash Service: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 以服务启动core
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target: "app", "正在尝试通过服务启动核心");
|
||||
log::info!(target: "app", "Attempting to start core via service");
|
||||
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
log::info!(target: "app", "Detected service version: {version}, required: {REQUIRED_SERVICE_VERSION}");
|
||||
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
log::warn!(target: "app", "Service version mismatch, reinstallation required");
|
||||
false
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配");
|
||||
log::info!(target: "app", "Service version matches");
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||
log::warn!(target: "app", "Failed to get service version: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if version_check && is_service_available().await.is_ok() {
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
log::info!(target: "app", "Service is running and version matches, attempting to use it");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
if !version_check {
|
||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||
log::info!(target: "app", "Service version mismatch, attempting reinstallation");
|
||||
|
||||
let service_state = ServiceState::get();
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target: "app", "由于限制无法重装服务");
|
||||
log::warn!(target: "app", "Cannot reinstall service due to limitations");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
|
||||
log::info!(target: "app", "Service started successfully despite version mismatch");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
bail!("Service version mismatch and cannot reinstall; startup failed");
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
log::info!(target: "app", "Starting service reinstallation");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
log::warn!(target: "app", "Service reinstallation failed: {err}");
|
||||
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
log::info!(target: "app", "Attempting to use existing service");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||
log::info!(target: "app", "Service reinstalled successfully, attempting to start");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
// Check service status
|
||||
match check_ipc_service_status().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
|
||||
log::info!(target: "app", "Service available but core not running, attempting to start");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||
log::warn!(target: "app", "Service check failed: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// 服务不可用或启动失败,检查是否需要重装
|
||||
// Service unavailable or startup failed, check if reinstallation is needed
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
log::info!(target: "app", "Service needs reinstallation");
|
||||
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
log::warn!(target: "app", "Service reinstallation failed: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
||||
log::info!(target: "app", "Service reinstallation completed, attempting to start core");
|
||||
start_with_existing_service(config_file).await
|
||||
} else {
|
||||
log::warn!(target: "app", "服务不可用且无法重装");
|
||||
log::warn!(target: "app", "Service unavailable and cannot be reinstalled");
|
||||
bail!("Service is not available and cannot be reinstalled at this time")
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过服务停止core
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "通过服务停止核心 (IPC)");
|
||||
logging!(info, Type::Service, true, "Stopping core via service (IPC)");
|
||||
|
||||
let payload = serde_json::json!({});
|
||||
let response = send_ipc_request(IpcCommand::StopClash, payload)
|
||||
.await
|
||||
.context("无法连接到Clash Verge Service")?;
|
||||
.context("Unable to connect to Koala Clash Service")?;
|
||||
|
||||
if !response.success {
|
||||
bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string()));
|
||||
bail!(response
|
||||
.error
|
||||
.unwrap_or_else(|| "Failed to stop core".to_string()));
|
||||
}
|
||||
|
||||
if let Some(data) = &response.data {
|
||||
@@ -924,18 +995,18 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
let msg = data
|
||||
.get("msg")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("未知错误");
|
||||
.unwrap_or("Unknown error");
|
||||
|
||||
if code_value != 0 {
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"停止核心返回错误: code={}, msg={}",
|
||||
"Stop core returned error: code={}, msg={}",
|
||||
code_value,
|
||||
msg
|
||||
);
|
||||
bail!("停止核心失败: {}", msg);
|
||||
bail!("Failed to stop core: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -945,19 +1016,24 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
|
||||
/// 检查服务是否正在运行
|
||||
pub async fn is_service_available() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "开始检查服务是否正在运行");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Checking whether service is running"
|
||||
);
|
||||
|
||||
match check_ipc_service_status().await {
|
||||
Ok(resp) => {
|
||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||
logging!(info, Type::Service, true, "服务正在运行");
|
||||
logging!(info, Type::Service, true, "Service is running");
|
||||
Ok(())
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
true,
|
||||
"服务未正常运行: code={}, msg={}",
|
||||
"Service not running normally: code={}, msg={}",
|
||||
resp.code,
|
||||
resp.msg
|
||||
);
|
||||
@@ -965,7 +1041,13 @@ pub async fn is_service_available() -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Service, true, "检查服务运行状态失败: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to check service running status: {}",
|
||||
err
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
@@ -973,21 +1055,21 @@ pub async fn is_service_available() -> Result<()> {
|
||||
|
||||
/// 强制重装服务(UI修复按钮)
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
log::info!(target: "app", "User requested forced service reinstallation");
|
||||
|
||||
let service_state = ServiceState::default();
|
||||
service_state.save()?;
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
log::info!(target: "app", "Service state reset, starting reinstallation");
|
||||
|
||||
match reinstall_service().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
log::info!(target: "app", "Service reinstalled successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
log::error!(target: "app", "Forced service reinstallation failed: {err}");
|
||||
bail!("Forced service reinstallation failed: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use sha2::{Digest, Sha256};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
|
||||
r"\\.\pipe\clash-verge-service"
|
||||
r"\\.\pipe\koala-clash-service"
|
||||
} else {
|
||||
"/tmp/clash-verge-service.sock"
|
||||
"/tmp/koala-clash-service.sock"
|
||||
};
|
||||
|
||||
// 定义命令类型
|
||||
@@ -43,7 +43,7 @@ pub struct IpcResponse {
|
||||
fn derive_secret_key() -> Vec<u8> {
|
||||
// to do
|
||||
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
|
||||
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight";
|
||||
let unique_app_id = "koala-clash-app-secret-fuck-me-until-daylight";
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(unique_app_id.as_bytes());
|
||||
hasher.finalize().to_vec()
|
||||
@@ -85,7 +85,7 @@ fn sign_message(message: &str) -> Result<String> {
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
let secret_key = derive_secret_key();
|
||||
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?;
|
||||
let mut mac = HmacSha256::new_from_slice(&secret_key).context("Failed to initialize HMAC")?;
|
||||
|
||||
mac.update(message.as_bytes());
|
||||
let result = mac.finalize();
|
||||
@@ -129,14 +129,25 @@ pub async fn send_ipc_request(
|
||||
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
|
||||
};
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Connecting to service (Windows)..."
|
||||
);
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to create signed request: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
@@ -147,8 +158,14 @@ pub async fn send_ipc_request(
|
||||
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
|
||||
Ok(name) => name,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "创建CString失败: {}", e);
|
||||
return Err(anyhow::anyhow!("创建CString失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to create CString: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to create CString: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,64 +187,110 @@ pub async fn send_ipc_request(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"连接到服务命名管道失败: {}",
|
||||
"Failed to connect to service named pipe: {}",
|
||||
error
|
||||
);
|
||||
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
|
||||
return Err(anyhow::anyhow!("Unable to connect to service named pipe: {}", error));
|
||||
}
|
||||
|
||||
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
|
||||
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service connection successful (Windows)"
|
||||
);
|
||||
|
||||
let request_bytes = request_json.as_bytes();
|
||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||
|
||||
if let Err(e) = pipe.write_all(&len_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to write request length: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to write request length: {}", e));
|
||||
}
|
||||
|
||||
if let Err(e) = pipe.write_all(request_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to write request body: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to write request body: {}", e));
|
||||
}
|
||||
|
||||
if let Err(e) = pipe.flush() {
|
||||
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
|
||||
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to flush pipe: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to flush pipe: {}", e));
|
||||
}
|
||||
|
||||
let mut response_len_bytes = [0u8; 4];
|
||||
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to read response length: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to read response length: {}", e));
|
||||
}
|
||||
|
||||
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
||||
|
||||
let mut response_bytes = vec![0u8; response_len];
|
||||
if let Err(e) = pipe.read_exact(&mut response_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to read response body: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to read response body: {}", e));
|
||||
}
|
||||
|
||||
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
|
||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to parse service response: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
match verify_response_signature(&response) {
|
||||
Ok(valid) => {
|
||||
if !valid {
|
||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||
bail!("服务响应签名验证失败");
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service response signature verification failed"
|
||||
);
|
||||
bail!("Service response signature verification failed");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Error verifying response signature: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@@ -236,7 +299,7 @@ pub async fn send_ipc_request(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC请求完成: 命令={}, 成功={}",
|
||||
"IPC request completed: command={}, success={}",
|
||||
command_type,
|
||||
response.success
|
||||
);
|
||||
@@ -255,14 +318,14 @@ pub async fn send_ipc_request(
|
||||
) -> Result<IpcResponse> {
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
||||
logging!(info, Type::Service, true, "Connecting to service (Unix)...");
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
||||
logging!(error, Type::Service, true, "Failed to create signed request: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
@@ -271,12 +334,23 @@ pub async fn send_ipc_request(
|
||||
|
||||
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
|
||||
Ok(s) => {
|
||||
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
|
||||
logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"Service connection successful (Unix)"
|
||||
);
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
|
||||
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to connect to Unix socket: {}",
|
||||
e
|
||||
);
|
||||
return Err(anyhow::anyhow!("Unable to connect to service Unix socket: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -284,46 +358,58 @@ pub async fn send_ipc_request(
|
||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||
|
||||
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to write request length: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to write request length: {}", e));
|
||||
}
|
||||
|
||||
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
|
||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to write request body: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to write request body: {}", e));
|
||||
}
|
||||
|
||||
let mut response_len_bytes = [0u8; 4];
|
||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to read response length: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to read response length: {}", e));
|
||||
}
|
||||
|
||||
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
||||
|
||||
let mut response_bytes = vec![0u8; response_len];
|
||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
|
||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
||||
logging!(error, Type::Service, true, "Failed to read response body: {}", e);
|
||||
return Err(anyhow::anyhow!("Failed to read response body: {}", e));
|
||||
}
|
||||
|
||||
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
|
||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Failed to parse service response: {}",
|
||||
e,
|
||||
);
|
||||
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
match verify_response_signature(&response) {
|
||||
Ok(valid) => {
|
||||
if !valid {
|
||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||
bail!("服务响应签名验证失败");
|
||||
logging!(error, Type::Service, true, "Service response signature verification failed");
|
||||
bail!("Service response signature verification failed");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Service,
|
||||
true,
|
||||
"Error verifying response signature: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@@ -332,7 +418,7 @@ pub async fn send_ipc_request(
|
||||
info,
|
||||
Type::Service,
|
||||
true,
|
||||
"IPC请求完成: 命令={}, 成功={}",
|
||||
"IPC request completed: command={}, success={}",
|
||||
command_type,
|
||||
response.success
|
||||
);
|
||||
|
||||
@@ -2,25 +2,21 @@
|
||||
use crate::utils::autostart as startup_shortcut;
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle::Handle,
|
||||
core::{handle::Handle, EventDrivenProxyManager},
|
||||
logging, logging_error,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri::async_runtime::Mutex as TokioMutex;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub struct Sysopt {
|
||||
update_sysproxy: Arc<TokioMutex<bool>>,
|
||||
reset_sysproxy: Arc<TokioMutex<bool>>,
|
||||
/// record whether the guard async is running or not
|
||||
guard_state: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -47,7 +43,7 @@ fn get_bypass() -> String {
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.to_string()
|
||||
} else if use_default {
|
||||
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
|
||||
format!("{DEFAULT_BYPASS},{custom_bypass}")
|
||||
} else {
|
||||
custom_bypass
|
||||
}
|
||||
@@ -59,12 +55,15 @@ impl Sysopt {
|
||||
SYSOPT.get_or_init(|| Sysopt {
|
||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||
guard_state: Arc::new(false.into()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||
self.guard_proxy();
|
||||
// 使用事件驱动代理管理器
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_app_started();
|
||||
|
||||
log::info!(target: "app", "Event-driven proxy guard enabled");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -101,12 +100,14 @@ impl Sysopt {
|
||||
};
|
||||
let mut auto = Autoproxy {
|
||||
enable: false,
|
||||
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
|
||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||
};
|
||||
|
||||
if !sys_enable {
|
||||
sys.set_system_proxy()?;
|
||||
auto.set_auto_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -115,6 +116,8 @@ impl Sysopt {
|
||||
auto.enable = true;
|
||||
sys.set_system_proxy()?;
|
||||
auto.set_auto_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -123,13 +126,18 @@ impl Sysopt {
|
||||
sys.enable = true;
|
||||
auto.set_auto_proxy()?;
|
||||
sys.set_system_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if !sys_enable {
|
||||
return self.reset_sysproxy().await;
|
||||
let result = self.reset_sysproxy().await;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return result;
|
||||
}
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use anyhow::bail;
|
||||
@@ -169,6 +177,8 @@ impl Sysopt {
|
||||
bail!("sysproxy exe run failed");
|
||||
}
|
||||
}
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -180,7 +190,16 @@ impl Sysopt {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
||||
let mut autoproxy = Autoproxy::get_auto_proxy()?;
|
||||
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "Failed to get auto proxy config while resetting: {e}, using default config");
|
||||
Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
sysproxy.enable = false;
|
||||
autoproxy.enable = false;
|
||||
autoproxy.set_auto_proxy()?;
|
||||
@@ -229,14 +248,14 @@ impl Sysopt {
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut() {
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "Failed to create startup shortcut: {}", e);
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "Failed to remove startup shortcut: {}", e);
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
@@ -271,11 +290,11 @@ impl Sysopt {
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
||||
log::info!(target: "app", "Shortcut auto-launch state: {}", enabled);
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
||||
log::error!(target: "app", "Failed to check shortcut, falling back to original method: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,176 +305,13 @@ impl Sysopt {
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {}", status);
|
||||
log::info!(target: "app", "Auto launch status: {status}");
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||
log::error!(target: "app", "Failed to get auto launch status: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_proxy(&self) {
|
||||
let _lock = self.guard_state.lock();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// default duration is 10s
|
||||
let mut wait_secs = 10u64;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_secs(wait_secs)).await;
|
||||
|
||||
let (enable, guard, guard_duration, pac, proxy_host) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.enable_proxy_guard.unwrap_or(false),
|
||||
verge.proxy_guard_duration.unwrap_or(10),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("127.0.0.1")),
|
||||
)
|
||||
};
|
||||
|
||||
// stop loop
|
||||
if !enable || !guard {
|
||||
continue;
|
||||
}
|
||||
|
||||
// update duration
|
||||
wait_secs = guard_duration;
|
||||
|
||||
log::debug!(target: "app", "try to guard the system proxy");
|
||||
|
||||
// 获取期望的代理端口
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
let bypass = get_bypass();
|
||||
|
||||
// 检查系统代理配置
|
||||
if pac {
|
||||
// 检查 PAC 代理设置
|
||||
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||
let autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get the auto proxy: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查自动代理是否启用且URL是否正确
|
||||
if !autoproxy.enable || autoproxy.url != expected_url {
|
||||
log::info!(target: "app", "auto proxy settings changed, restoring...");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_autoproxy = Autoproxy {
|
||||
enable: true,
|
||||
url: expected_url,
|
||||
};
|
||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get service path: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["pac", expected_url.as_str()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "failed to set auto proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查常规系统代理设置
|
||||
let sysproxy = match Sysproxy::get_system_proxy() {
|
||||
Ok(sp) => sp,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get the system proxy: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查系统代理是否启用且配置是否匹配
|
||||
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
|
||||
log::info!(target: "app", "system proxy settings changed, restoring...");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_sysproxy = Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host.clone(),
|
||||
port,
|
||||
bypass: bypass.clone(),
|
||||
};
|
||||
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get service path: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", proxy_host, port);
|
||||
let shell = app_handle.shell();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["global", address.as_str(), bypass.as_ref()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "failed to set system proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"已注册的定时任务数量: {}",
|
||||
"Registered timer task count: {}",
|
||||
timer_map.len()
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
||||
"Registered timer task - uid={}, interval={}min, task_id={}",
|
||||
uid,
|
||||
task.interval_minutes,
|
||||
task.task_id
|
||||
@@ -100,7 +100,12 @@ impl Timer {
|
||||
let uid = item.uid.as_ref()?;
|
||||
|
||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"Profile requires immediate update: uid={}",
|
||||
uid
|
||||
);
|
||||
Some(uid.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -116,7 +121,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"需要立即更新的配置数量: {}",
|
||||
"Number of profiles requiring immediate update: {}",
|
||||
profiles_to_update.len()
|
||||
);
|
||||
let timer_map = self.timer_map.read();
|
||||
@@ -124,7 +129,7 @@ impl Timer {
|
||||
|
||||
for uid in profiles_to_update {
|
||||
if let Some(task) = timer_map.get(&uid) {
|
||||
logging!(info, Type::Timer, "立即执行任务: uid={}", uid);
|
||||
logging!(info, Type::Timer, "Executing task immediately: uid={}", uid);
|
||||
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
||||
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
|
||||
}
|
||||
@@ -237,7 +242,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"找到定时更新配置: uid={}, interval={}min",
|
||||
"Found scheduled update config: uid={}, interval={}min",
|
||||
uid,
|
||||
interval
|
||||
);
|
||||
@@ -251,7 +256,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"生成的定时更新配置数量: {}",
|
||||
"Generated scheduled update config count: {}",
|
||||
new_map.len()
|
||||
);
|
||||
new_map
|
||||
@@ -267,7 +272,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"当前 timer_map 大小: {}",
|
||||
"Current timer_map size: {}",
|
||||
timer_map.len()
|
||||
);
|
||||
|
||||
@@ -279,7 +284,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"定时任务间隔变更: uid={}, 旧={}, 新={}",
|
||||
"Timer task interval changed: uid={}, old={}, new={}",
|
||||
uid,
|
||||
task.interval_minutes,
|
||||
interval
|
||||
@@ -288,12 +293,12 @@ impl Timer {
|
||||
}
|
||||
None => {
|
||||
// Task no longer needed
|
||||
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid);
|
||||
logging!(debug, Type::Timer, "Timer task removed: uid={}", uid);
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
|
||||
}
|
||||
_ => {
|
||||
// Task exists with same interval, no change needed
|
||||
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid);
|
||||
logging!(debug, Type::Timer, "Timer task unchanged: uid={}", uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,7 +311,7 @@ impl Timer {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"新增定时任务: uid={}, interval={}min",
|
||||
"Added timer task: uid={}, interval={}min",
|
||||
uid,
|
||||
interval
|
||||
);
|
||||
@@ -320,7 +325,13 @@ impl Timer {
|
||||
*self.timer_count.lock() = next_id;
|
||||
}
|
||||
|
||||
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
||||
logging!(debug, Type::Timer, "Number of scheduled task changes: {}", diff_map.len());
|
||||
logging!(
|
||||
debug,
|
||||
Type::Timer,
|
||||
"Number of timer task changes: {}",
|
||||
diff_map.len()
|
||||
);
|
||||
diff_map
|
||||
}
|
||||
|
||||
@@ -363,13 +374,18 @@ impl Timer {
|
||||
|
||||
/// Get next update time for a profile
|
||||
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
|
||||
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
|
||||
logging!(info, Type::Timer, "Getting next update time, uid={}", uid);
|
||||
|
||||
let timer_map = self.timer_map.read();
|
||||
let task = match timer_map.get(uid) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Corresponding timer task not found, uid={}",
|
||||
uid
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -380,7 +396,7 @@ impl Timer {
|
||||
let items = match profiles.get_items() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "获取配置列表失败");
|
||||
logging!(warn, Type::Timer, "Failed to get profile list");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -388,7 +404,12 @@ impl Timer {
|
||||
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "找不到对应的配置,uid={}", uid);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Corresponding profile not found, uid={}",
|
||||
uid
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -401,7 +422,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"计算得到下次更新时间: {}, uid={}",
|
||||
"Calculated next update time: {}, uid={}",
|
||||
next_time,
|
||||
uid
|
||||
);
|
||||
@@ -410,7 +431,7 @@ impl Timer {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"更新时间或间隔无效,updated={}, interval={}",
|
||||
"Invalid update time or interval, updated={}, interval={}",
|
||||
updated,
|
||||
task.interval_minutes
|
||||
);
|
||||
@@ -442,7 +463,7 @@ impl Timer {
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"配置 {} 是否为当前激活配置: {}",
|
||||
"Is profile {} currently active: {}",
|
||||
uid,
|
||||
is_current
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
|
||||
@@ -12,15 +11,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
#[cfg(target_os = "macos")]
|
||||
use futures::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(target_os = "macos")]
|
||||
use parking_lot::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
@@ -31,20 +22,37 @@ use tauri::{
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||
AppHandle, Wry,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::handle;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrayState {}
|
||||
|
||||
// 托盘点击防抖机制
|
||||
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||
|
||||
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||
TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1)))
|
||||
}
|
||||
|
||||
fn should_handle_tray_click() -> bool {
|
||||
let debounce_lock = get_tray_click_debounce();
|
||||
let mut last_click = debounce_lock.lock();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
|
||||
*last_click = now;
|
||||
true
|
||||
} else {
|
||||
log::debug!(target: "app", "Tray click ignored by debounce; time since last click: {:?}ms",
|
||||
now.duration_since(*last_click).as_millis());
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub struct Tray {
|
||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
last_menu_update: Mutex<Option<Instant>>,
|
||||
menu_updating: AtomicBool,
|
||||
}
|
||||
@@ -105,7 +113,7 @@ impl TrayState {
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
|
||||
include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -139,7 +147,7 @@ impl TrayState {
|
||||
if tray_icon_colorful == "monochrome" {
|
||||
(
|
||||
false,
|
||||
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
|
||||
include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -164,10 +172,6 @@ impl Tray {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return TRAY.get_or_init(|| Tray {
|
||||
speed_rate: Arc::new(Mutex::new(None)),
|
||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||
is_subscribed: Arc::new(RwLock::new(false)),
|
||||
rate_cache: Arc::new(Mutex::new(None)),
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
});
|
||||
@@ -180,11 +184,6 @@ impl Tray {
|
||||
}
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut speed_rate = self.speed_rate.lock();
|
||||
*speed_rate = Some(SpeedRate::new());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -231,7 +230,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray menu: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -279,11 +278,11 @@ impl Tray {
|
||||
profile_uid_and_name,
|
||||
is_lightweight_mode,
|
||||
)?));
|
||||
log::debug!(target: "app", "托盘菜单更新成功");
|
||||
log::debug!(target: "app", "Tray menu updated successfully");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray menu: tray not found");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -291,11 +290,11 @@ impl Tray {
|
||||
|
||||
/// 更新托盘图标
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -303,7 +302,7 @@ impl Tray {
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: tray not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -312,55 +311,18 @@ impl Tray {
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(),
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
};
|
||||
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
let is_colorful = colorful == "colorful";
|
||||
|
||||
if !enable_tray_speed {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(guard) = guard.as_ref() {
|
||||
if let Some(rate) = guard.get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = if enable_tray_icon {
|
||||
Some(icon_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rate = rate_guard.as_ref();
|
||||
if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
||||
}
|
||||
}
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -369,7 +331,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -377,7 +339,7 @@ impl Tray {
|
||||
let tray = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray icon: tray not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -413,7 +375,7 @@ impl Tray {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
|
||||
log::warn!(target: "app", "Failed to update tray tooltip: app_handle not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -421,7 +383,7 @@ impl Tray {
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
|
||||
log::warn!(target: "app", "Failed to update tray tooltip: version info not found");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -451,7 +413,7 @@ impl Tray {
|
||||
|
||||
if let Some(tray) = app_handle.tray_by_id("main") {
|
||||
let _ = tray.set_tooltip(Some(&format!(
|
||||
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
"Koala Clash {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
t("SysProxy"),
|
||||
switch_map[system_proxy],
|
||||
t("TUN"),
|
||||
@@ -460,7 +422,7 @@ impl Tray {
|
||||
current_profile_name
|
||||
)));
|
||||
} else {
|
||||
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
|
||||
log::warn!(target: "app", "Failed to update tray tooltip: tray not found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -475,158 +437,12 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 订阅流量数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn subscribe_traffic(&self) -> Result<()> {
|
||||
log::info!(target: "app", "subscribe traffic");
|
||||
|
||||
// 如果已经订阅,先取消订阅
|
||||
if *self.is_subscribed.read() {
|
||||
self.unsubscribe_traffic();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(3);
|
||||
*self.shutdown_tx.write() = Some(shutdown_tx);
|
||||
*self.is_subscribed.write() = true;
|
||||
|
||||
let speed_rate = Arc::clone(&self.speed_rate);
|
||||
let is_subscribed = Arc::clone(&self.is_subscribed);
|
||||
|
||||
// 使用单线程防止阻塞主线程
|
||||
std::thread::Builder::new()
|
||||
.name("traffic-monitor".into())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to build tokio runtime for traffic monitor");
|
||||
// 在单独的运行时中执行异步任务
|
||||
rt.block_on(async move {
|
||||
let mut shutdown = shutdown_rx;
|
||||
let speed_rate = speed_rate.clone();
|
||||
let is_subscribed = is_subscribed.clone();
|
||||
let mut consecutive_errors = 0;
|
||||
let max_consecutive_errors = 5;
|
||||
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
|
||||
|
||||
'outer: loop {
|
||||
if !*is_subscribed.read() {
|
||||
log::info!(target: "app", "Traffic subscription has been cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
Traffic::get_traffic_stream()
|
||||
).await {
|
||||
Ok(stream_result) => {
|
||||
match stream_result {
|
||||
Ok(mut stream) => {
|
||||
consecutive_errors = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
traffic_result = stream.next() => {
|
||||
match traffic_result {
|
||||
Some(Ok(traffic)) => {
|
||||
if let Ok(Some(rate)) = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(50),
|
||||
async {
|
||||
let guard = speed_rate.try_lock();
|
||||
if let Some(guard) = guard {
|
||||
if let Some(sr) = guard.as_ref() {
|
||||
sr.update_and_check_changed(traffic.up, traffic.down)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
).await {
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(100),
|
||||
async { let _ = Tray::global().update_icon(Some(rate)); }
|
||||
).await;
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => {
|
||||
log::error!(target: "app", "Traffic stream error: {}", e);
|
||||
consecutive_errors += 1;
|
||||
if consecutive_errors >= max_consecutive_errors {
|
||||
log::error!(target: "app", "Too many errors, reconnecting traffic stream");
|
||||
break;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
log::info!(target: "app", "Traffic stream ended, reconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
log::info!(target: "app", "Received shutdown signal for traffic stream");
|
||||
break 'outer;
|
||||
},
|
||||
_ = interval.tick() => {
|
||||
if !*is_subscribed.read() {
|
||||
log::info!(target: "app", "Traffic monitor detected subscription cancelled");
|
||||
break 'outer;
|
||||
}
|
||||
log::debug!(target: "app", "Traffic subscription periodic health check");
|
||||
},
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
|
||||
log::info!(target: "app", "Traffic stream max active time reached, reconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get traffic stream: {}", e);
|
||||
consecutive_errors += 1;
|
||||
if consecutive_errors >= max_consecutive_errors {
|
||||
log::error!(target: "app", "Too many consecutive errors, pausing traffic monitoring");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
consecutive_errors = 0;
|
||||
} else {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
log::error!(target: "app", "Traffic stream initialization timed out");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
if !*is_subscribed.read() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
log::info!(target: "app", "Traffic subscription thread terminated");
|
||||
});
|
||||
})
|
||||
.expect("Failed to spawn traffic monitor thread");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消订阅 traffic 数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn unsubscribe_traffic(&self) {
|
||||
log::info!(target: "app", "unsubscribe traffic");
|
||||
*self.is_subscribed.write() = false;
|
||||
if let Some(tx) = self.shutdown_tx.write().take() {
|
||||
drop(tx);
|
||||
}
|
||||
}
|
||||
pub fn unsubscribe_traffic(&self) {}
|
||||
|
||||
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
||||
log::info!(target: "app", "Creating system tray from AppHandle");
|
||||
|
||||
// 获取图标
|
||||
let icon_bytes = TrayState::get_common_tray_icon().1;
|
||||
@@ -656,7 +472,7 @@ impl Tray {
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app","tray event: {:?}", tray_event);
|
||||
log::debug!(target: "app","tray event: {tray_event:?}");
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
@@ -664,25 +480,30 @@ impl Tray {
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// 添加防抖检查,防止快速连击
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"main_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
||||
log::info!(target: "app", "Tray click: show main window");
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
||||
log::info!(target: "app", "Window show result: {result:?}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
log::info!(target: "app", "系统托盘创建成功");
|
||||
log::info!(target: "app", "System tray created successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -736,7 +557,7 @@ fn create_tray_menu(
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("profiles_{}", profile_uid),
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
@@ -779,16 +600,6 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"direct_mode",
|
||||
t("Direct Mode"),
|
||||
true,
|
||||
mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
@@ -828,45 +639,6 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_env =
|
||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_app_dir",
|
||||
t("Conf Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_core_dir",
|
||||
t("Core Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_logs_dir",
|
||||
t("Logs Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"open_dir",
|
||||
t("Open Dir"),
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_clash",
|
||||
@@ -914,7 +686,6 @@ fn create_tray_menu(
|
||||
separator,
|
||||
rule_mode,
|
||||
global_mode,
|
||||
direct_mode,
|
||||
separator,
|
||||
profiles,
|
||||
separator,
|
||||
@@ -922,8 +693,6 @@ fn create_tray_menu(
|
||||
tun_mode,
|
||||
separator,
|
||||
lighteweight_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
separator,
|
||||
quit,
|
||||
@@ -948,15 +717,18 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
"open_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
// 如果在轻量模式中,先退出轻量模式
|
||||
log::info!(target: "app", "Tray menu click: open window");
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
// 使用统一的窗口管理器显示窗口
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {:?}", result);
|
||||
log::info!(target: "app", "Window show result: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy();
|
||||
@@ -964,20 +736,13 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
"tun_mode" => {
|
||||
feat::toggle_tun_mode(None);
|
||||
}
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => {
|
||||
let _ = cmd::open_app_dir();
|
||||
}
|
||||
"open_core_dir" => {
|
||||
let _ = cmd::open_core_dir();
|
||||
}
|
||||
"open_logs_dir" => {
|
||||
let _ = cmd::open_logs_dir();
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => {
|
||||
// 处理轻量模式的切换
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
||||
if was_lightweight {
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
@@ -985,11 +750,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
crate::module::lightweight::entry_lightweight_mode();
|
||||
}
|
||||
|
||||
// 退出轻量模式后显示主窗口
|
||||
if was_lightweight {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {:?}", result);
|
||||
log::info!(target: "app", "Show main window after exiting lightweight mode: {result:?}");
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
@@ -1002,8 +766,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 统一调用状态更新
|
||||
if let Err(e) = Tray::global().update_all_states() {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {}", e);
|
||||
log::warn!(target: "app", "Failed to update tray state: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,336 +1 @@
|
||||
use crate::{
|
||||
module::mihomo::{MihomoManager, Rate},
|
||||
utils::help::format_bytes_speed,
|
||||
};
|
||||
use ab_glyph::FontArc;
|
||||
use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use image::{GenericImageView, Rgba, RgbaImage};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use parking_lot::Mutex;
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
use tokio_tungstenite::tungstenite::http;
|
||||
use tungstenite::client::IntoClientRequest;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
|
||||
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
|
||||
let mut rates = self.rate.lock();
|
||||
let mut last_update = self.last_update.lock();
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
// 限制更新频率为每秒最多2次(500ms)
|
||||
if now.duration_since(*last_update).as_millis() < 500 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (current, previous) = &mut *rates;
|
||||
|
||||
// Avoid unnecessary float conversions for small value checks
|
||||
let should_update = if current.up < 1000 && down < 1000 {
|
||||
// For small values, always update to ensure accuracy
|
||||
current.up != up || current.down != down
|
||||
} else {
|
||||
// For larger values, use integer math to check for >5% change
|
||||
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
|
||||
let up_threshold = current.up / 20;
|
||||
let down_threshold = current.down / 20;
|
||||
|
||||
(up > current.up && up - current.up > up_threshold)
|
||||
|| (up < current.up && current.up - up > up_threshold)
|
||||
|| (down > current.down && down - current.down > down_threshold)
|
||||
|| (down < current.down && current.down - down > down_threshold)
|
||||
};
|
||||
|
||||
if !should_update {
|
||||
return None;
|
||||
}
|
||||
|
||||
*previous = current.clone();
|
||||
current.up = up;
|
||||
current.down = down;
|
||||
*last_update = now;
|
||||
|
||||
if previous != current {
|
||||
Some(current.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_curent_rate(&self) -> Option<Rate> {
|
||||
let rates = self.rate.lock();
|
||||
let (current, _) = &*rates;
|
||||
Some(current.clone())
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(
|
||||
is_custom_icon: bool,
|
||||
icon_bytes: Option<Vec<u8>>,
|
||||
rate: Option<&Rate>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||
|
||||
let (mut icon_width, mut icon_height) = (0, 256);
|
||||
let icon_image = if let Some(bytes) = icon_bytes.clone() {
|
||||
let icon_image = image::load_from_memory(&bytes)?;
|
||||
icon_width = icon_image.width();
|
||||
icon_height = icon_image.height();
|
||||
icon_image
|
||||
} else {
|
||||
// 返回一个空的 RGBA 图像
|
||||
image::DynamicImage::new_rgba8(0, 0)
|
||||
};
|
||||
|
||||
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
|
||||
(true, true) => 510,
|
||||
(true, false) => 740,
|
||||
(false, false) => 740,
|
||||
(false, true) => icon_width + 740,
|
||||
};
|
||||
|
||||
// println!(
|
||||
// "icon_height: {}, icon_wight: {}, total_width: {}",
|
||||
// icon_height, icon_width, total_width
|
||||
// );
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
||||
// 将原始图标绘制到新画布的左侧
|
||||
if icon_bytes.is_some() {
|
||||
for y in 0..icon_height {
|
||||
for x in 0..icon_width {
|
||||
let pixel = icon_image.get_pixel(x, y);
|
||||
combined_image.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_colorful = if let Some(bytes) = icon_bytes.clone() {
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 选择文本颜色
|
||||
let (text_color, shadow_color) = if is_colorful {
|
||||
(
|
||||
Rgba([144u8, 144u8, 144u8, 255u8]),
|
||||
// Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||
)
|
||||
// (
|
||||
// Rgba([160u8, 160u8, 160u8, 255u8]),
|
||||
// // Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||
// Rgba([0u8, 0u8, 0u8, 255u8]),
|
||||
// )
|
||||
} else {
|
||||
(
|
||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||
)
|
||||
};
|
||||
// 减小字体大小以适应文本区域
|
||||
let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf");
|
||||
let font = FontArc::try_from_vec(font_data.to_vec()).unwrap();
|
||||
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
||||
let scale = ab_glyph::PxScale::from(font_size);
|
||||
|
||||
// 使用更简洁的速率格式
|
||||
let up_text = format!("↑ {}", format_bytes_speed(rate.up));
|
||||
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
|
||||
|
||||
// For test rate display
|
||||
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
|
||||
|
||||
// 计算文本位置,确保垂直间距合适
|
||||
// 修改文本位置为居右显示
|
||||
// 计算右对齐的文本位置
|
||||
// let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
||||
// let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
||||
// let up_text_x = total_width - up_text_width;
|
||||
// let down_text_x = total_width - down_text_width;
|
||||
|
||||
// 计算左对齐的文本位置
|
||||
let (up_text_x, down_text_x) = {
|
||||
if is_custom_icon || icon_bytes.is_some() {
|
||||
let text_left_offset = 30;
|
||||
let left_begin = icon_width + text_left_offset;
|
||||
(left_begin, left_begin)
|
||||
} else {
|
||||
(icon_width, icon_width)
|
||||
}
|
||||
};
|
||||
|
||||
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
||||
let text_height = font_size as i32;
|
||||
let total_text_height = text_height * 2;
|
||||
let up_y = (icon_height as i32 - total_text_height) / 2;
|
||||
let down_y = up_y + text_height;
|
||||
|
||||
// 绘制速率文本(先阴影后文字)
|
||||
let shadow_offset = 1;
|
||||
|
||||
// 绘制上行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
up_text_x as i32 + shadow_offset,
|
||||
up_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
up_text_x as i32,
|
||||
up_y,
|
||||
scale,
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
|
||||
// 绘制下行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
shadow_color,
|
||||
down_text_x as i32 + shadow_offset,
|
||||
down_y + shadow_offset,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
text_color,
|
||||
down_text_x as i32,
|
||||
down_y,
|
||||
scale,
|
||||
&font,
|
||||
&down_text,
|
||||
);
|
||||
|
||||
// 将结果转换为 PNG 数据
|
||||
let mut bytes = Vec::new();
|
||||
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Traffic {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
impl Traffic {
|
||||
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
||||
{
|
||||
use futures::{
|
||||
future::FutureExt,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
// 先处理错误和超时情况
|
||||
let stream = Box::pin(
|
||||
stream::unfold((), move |_| async move {
|
||||
'retry: loop {
|
||||
log::info!(target: "app", "establishing traffic websocket connection");
|
||||
let (url, token) = MihomoManager::get_traffic_ws_url();
|
||||
let mut request = match url.into_client_request() {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to create websocket request: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
continue 'retry;
|
||||
}
|
||||
};
|
||||
|
||||
request.headers_mut().insert(http::header::AUTHORIZATION, token);
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(3),
|
||||
tokio_tungstenite::connect_async(request)
|
||||
).await {
|
||||
Ok(Ok((ws_stream, _))) => {
|
||||
log::info!(target: "app", "traffic websocket connection established");
|
||||
// 设置流超时控制
|
||||
let traffic_stream = ws_stream
|
||||
.take_while(|msg| {
|
||||
let continue_stream = msg.is_ok();
|
||||
async move { continue_stream }.boxed()
|
||||
})
|
||||
.filter_map(|msg| async move {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
if !msg.is_text() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(200),
|
||||
async { msg.into_text() }
|
||||
).await {
|
||||
Ok(Ok(text)) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&text) {
|
||||
Ok(json) => {
|
||||
let up = json["up"].as_u64().unwrap_or(0);
|
||||
let down = json["down"].as_u64().unwrap_or(0);
|
||||
Some(Ok(Traffic { up, down }))
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "traffic json parse error: {} for {}", e, text);
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "traffic text conversion error: {}", e);
|
||||
None
|
||||
},
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "traffic text processing timeout");
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "traffic websocket error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Some((traffic_stream, ()));
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
log::error!(target: "app", "traffic websocket connection failed: {}", e);
|
||||
},
|
||||
Err(_) => {
|
||||
log::error!(target: "app", "traffic websocket connection timed out");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ impl ChainSupport {
|
||||
(self, core.as_str()),
|
||||
(ChainSupport::All, _)
|
||||
| (ChainSupport::Clash, "clash")
|
||||
| (ChainSupport::ClashMeta, "verge-mihomo")
|
||||
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
| (ChainSupport::ClashMeta, "koala-mihomo")
|
||||
| (ChainSupport::ClashMetaAlpha, "koala-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
}
|
||||
|
||||
@@ -202,7 +202,9 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
});
|
||||
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
|
||||
for (key, value) in patch_tun.into_iter() {
|
||||
tun.insert(key, value);
|
||||
if !tun.contains_key(&key) {
|
||||
tun.insert(key, value);
|
||||
}
|
||||
}
|
||||
config.insert("tun".into(), tun.into());
|
||||
} else {
|
||||
@@ -239,7 +241,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
.map(|(_, c)| c)
|
||||
.for_each(|item| {
|
||||
log::debug!(target: "app", "run builtin script {}", item.uid);
|
||||
log::debug!(target: "app", "run builtin script {0}", item.uid);
|
||||
if let ChainType::Script(script) = item.data {
|
||||
match use_script(script, config.to_owned(), "".to_string()) {
|
||||
Ok((res_config, _)) => {
|
||||
|
||||
@@ -141,8 +141,8 @@ fn test_script() {
|
||||
fn test_escape_unescape() {
|
||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||
let escaped = escape_js_string_for_single_quote(test_string);
|
||||
println!("Original: {}", test_string);
|
||||
println!("Escaped: {}", escaped);
|
||||
println!("Original: {test_string}");
|
||||
println!("Escaped: {escaped}");
|
||||
|
||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||
let parsed = parse_json_safely(json_str).unwrap();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
crate::utils::resolve::set_public_dns("8.8.8.8".to_string()).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::fs;
|
||||
/// Create a backup and upload to WebDAV
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to create backup: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
||||
log::warn!(target: "app", "Failed to remove temp file: {err:#?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
/// List WebDAV backups
|
||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {err:#?}");
|
||||
err
|
||||
})
|
||||
}
|
||||
@@ -44,7 +44,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {err:#?}");
|
||||
err
|
||||
})
|
||||
}
|
||||
@@ -62,7 +62,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ fn after_change_clash_mode() {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to get connections: {}", err);
|
||||
log::error!(target: "app", "Failed to get connections: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -108,12 +108,12 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
let start = Instant::now();
|
||||
|
||||
let response = NetworkManager::global()
|
||||
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
|
||||
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false, false)
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
log::trace!(target: "app", "test_delay response: {response:#?}");
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
@@ -121,7 +121,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
log::trace!(target: "app", "test_delay error: {err:#?}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,13 @@ pub async fn update_profile(
|
||||
option: Option<PrfOption>,
|
||||
auto_refresh: Option<bool>,
|
||||
) -> Result<()> {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Start updating subscription {}",
|
||||
uid
|
||||
);
|
||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||
|
||||
let url_opt = {
|
||||
@@ -41,14 +47,14 @@ pub async fn update_profile(
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
log::info!(target: "app", "[Subscription Update] {uid} is not a remote subscription, skipping update");
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
log::warn!(target: "app", "[Subscription Update] {uid} is missing URL, cannot update");
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
log::info!(target: "app",
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
"[Subscription Update] {} is a remote subscription, URL: {}",
|
||||
uid,
|
||||
item.url.clone().unwrap()
|
||||
);
|
||||
@@ -58,24 +64,24 @@ pub async fn update_profile(
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||
log::info!(target: "app", "[Subscription Update] Start downloading new subscription content");
|
||||
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||
|
||||
// 尝试使用正常设置更新
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
log::info!(target: "app", "[Subscription Update] Subscription config updated successfully");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(err) => {
|
||||
// 首次更新失败,尝试使用Clash代理
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||
log::warn!(target: "app", "[Subscription Update] Normal update failed: {err}, trying to update via Clash proxy");
|
||||
|
||||
// 发送通知
|
||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||
@@ -92,7 +98,7 @@ pub async fn update_profile(
|
||||
// 使用Clash代理重试
|
||||
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||
Ok(mut item) => {
|
||||
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||
log::info!(target: "app", "[Subscription Update] Update via Clash proxy succeeded");
|
||||
|
||||
// 恢复原始代理设置到item
|
||||
if let Some(option) = item.option.as_mut() {
|
||||
@@ -112,14 +118,14 @@ pub async fn update_profile(
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(retry_err) => {
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||
log::error!(target: "app", "[Subscription Update] Update via Clash proxy still failed: {retry_err}");
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{}", retry_err),
|
||||
format!("{retry_err}"),
|
||||
);
|
||||
return Err(retry_err);
|
||||
}
|
||||
@@ -131,14 +137,30 @@ pub async fn update_profile(
|
||||
};
|
||||
|
||||
if should_update {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Update core configuration"
|
||||
);
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Update succeeded"
|
||||
);
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[Subscription Update] Update failed: {}",
|
||||
err
|
||||
);
|
||||
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn toggle_system_proxy() {
|
||||
.close_all_connections()
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to close all connections: {}", err);
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ pub fn copy_clash_env() {
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
||||
|
||||
let cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
|
||||
@@ -11,29 +11,60 @@ use crate::{
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
open_or_close_dashboard_internal(false)
|
||||
}
|
||||
|
||||
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard_hotkey() {
|
||||
open_or_close_dashboard_internal(true)
|
||||
}
|
||||
|
||||
/// Internal implementation for opening/closing dashboard
|
||||
fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard (bypass debounce: {bypass_debounce})");
|
||||
|
||||
// 检查是否在轻量模式下
|
||||
// 热键调用调度到主线程执行,避免 WebView 创建死锁
|
||||
if bypass_debounce {
|
||||
log::info!(target: "app", "Hotkey invoked, dispatching window operation to main thread");
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::info!(target: "app", "Executing hotkey window operation on main thread");
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "Window operation result: {result:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = WindowManager::toggle_main_window();
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "Window operation result: {:?}", result);
|
||||
log::info!(target: "app", "Window operation result: {result:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用统一的窗口管理器切换窗口状态
|
||||
let result = WindowManager::toggle_main_window();
|
||||
log::info!(target: "app", "Window toggle result: {:?}", result);
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
/// 异步优化的应用退出函数
|
||||
pub fn quit() {
|
||||
use crate::process::AsyncHandler;
|
||||
logging!(debug, Type::System, true, "启动退出流程");
|
||||
logging!(debug, Type::System, true, "Start exit process");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
@@ -42,19 +73,24 @@ pub fn quit() {
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
log::info!(target: "app", "窗口已隐藏");
|
||||
log::info!(target: "app", "Window hidden");
|
||||
}
|
||||
|
||||
// 使用异步任务处理资源清理,避免阻塞
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始异步清理资源");
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"Start asynchronous resource cleanup"
|
||||
);
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"资源清理完成,退出代码: {}",
|
||||
"Resource cleanup completed, exit code: {}",
|
||||
if cleanup_result { 0 } else { 1 }
|
||||
);
|
||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||
@@ -64,7 +100,12 @@ pub fn quit() {
|
||||
async fn clean_async() -> bool {
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"Start executing asynchronous cleanup..."
|
||||
);
|
||||
|
||||
// 1. 处理TUN模式
|
||||
let tun_task = async {
|
||||
@@ -81,11 +122,11 @@ async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "TUN模式已禁用");
|
||||
log::info!(target: "app", "TUN mode disabled");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "禁用TUN模式超时");
|
||||
log::warn!(target: "app", "Timeout disabling TUN mode");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -103,11 +144,11 @@ async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "系统代理已重置");
|
||||
log::info!(target: "app", "System proxy reset");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "重置系统代理超时");
|
||||
log::warn!(target: "app", "Timeout resetting system proxy");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -117,11 +158,11 @@ async fn clean_async() -> bool {
|
||||
let core_task = async {
|
||||
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "核心服务已停止");
|
||||
log::info!(target: "app", "Core service stopped");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "停止核心服务超时");
|
||||
log::warn!(target: "app", "Timeout stopping core service");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -137,11 +178,11 @@ async fn clean_async() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "DNS设置已恢复");
|
||||
log::info!(target: "app", "DNS settings restored");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "恢复DNS设置超时");
|
||||
log::warn!(target: "app", "Timeout restoring DNS settings");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -161,7 +202,7 @@ async fn clean_async() -> bool {
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
||||
"Asynchronous cleanup completed - TUN: {}, Proxy: {}, Core: {}, DNS: {}, Overall: {}",
|
||||
tun_success,
|
||||
proxy_success,
|
||||
core_success,
|
||||
@@ -178,7 +219,7 @@ pub fn clean() -> bool {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始执行清理操作...");
|
||||
logging!(info, Type::System, true, "Start executing cleanup...");
|
||||
|
||||
// 使用已有的异步清理函数
|
||||
let cleanup_result = clean_async().await;
|
||||
@@ -189,7 +230,13 @@ pub fn clean() -> bool {
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
||||
Ok(result) => {
|
||||
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"Cleanup completed, result: {}",
|
||||
result
|
||||
);
|
||||
result
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -197,7 +244,7 @@ pub fn clean() -> bool {
|
||||
warn,
|
||||
Type::System,
|
||||
true,
|
||||
"清理操作超时,返回成功状态避免阻塞"
|
||||
"Cleanup timed out, returning success to avoid blocking"
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ mod module;
|
||||
mod process;
|
||||
mod state;
|
||||
mod utils;
|
||||
use crate::{
|
||||
core::hotkey,
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
};
|
||||
use crate::{core::hotkey, process::AsyncHandler, utils::resolve};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use tauri::AppHandle;
|
||||
@@ -86,84 +82,75 @@ impl AppHandleManager {
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub fn run() {
|
||||
utils::network::NetworkManager::global().init();
|
||||
// Capture early deep link before any async setup (cold start on macOS)
|
||||
utils::resolve::capture_early_deep_link_from_args();
|
||||
|
||||
utils::network::NetworkManager::global().init();
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
// 异步单例检测
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
if let Some(app_handle) = AppHandleManager::global().get() {
|
||||
app_handle.exit(0);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
}
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"单例检查超时,假定没有其他实例运行"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let devtools = tauri_plugin_devtools::init();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
}
|
||||
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
|
||||
// Handle deep link when a second instance is invoked: forward URL to the running instance
|
||||
if let Some(url) = argv
|
||||
.iter()
|
||||
.find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://"))
|
||||
.cloned()
|
||||
{
|
||||
// Robust scheduling avoids races with lightweight/window
|
||||
resolve::schedule_handle_deep_link(url);
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "Starting app initialization...");
|
||||
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
logging!(info, Type::Setup, true, "注册深层链接...");
|
||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||
}
|
||||
// Register deep link handler as early as possible to not miss cold-start events (macOS)
|
||||
app.deep_link().on_open_url(|event| {
|
||||
let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect();
|
||||
logging!(info, Type::Setup, true, "on_open_url received: {:?}", urls);
|
||||
if let Some(url) = urls.first().cloned() {
|
||||
resolve::schedule_handle_deep_link(url);
|
||||
}
|
||||
});
|
||||
|
||||
app.deep_link().on_open_url(|event| {
|
||||
AsyncHandler::spawn(move || {
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
async move {
|
||||
if let Some(url) = url {
|
||||
logging_error!(Type::Setup, true, resolve_scheme(url).await);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
}
|
||||
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
||||
|
||||
// Ensure URL schemes are registered with the OS (all platforms)
|
||||
logging!(info, Type::Setup, true, "Registering deep links with OS...");
|
||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||
|
||||
// Deep link handler will be registered AFTER core handle init to ensure window creation works
|
||||
|
||||
// 窗口管理
|
||||
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initializing window state management..."
|
||||
);
|
||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||
.with_filename("window_state.json")
|
||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||
@@ -173,7 +160,12 @@ pub fn run() {
|
||||
// 异步处理
|
||||
let app_handle = app.handle().clone();
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Executing app setup asynchronously..."
|
||||
);
|
||||
match timeout(
|
||||
Duration::from_secs(30),
|
||||
resolve::resolve_setup_async(&app_handle),
|
||||
@@ -181,41 +173,81 @@ pub fn run() {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Setup, true, "应用设置成功完成");
|
||||
logging!(info, Type::Setup, true, "App setup completed successfully");
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"应用设置超时(30秒),继续执行后续流程"
|
||||
"App setup timed out (30s), continuing with subsequent steps"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Executing main setup operations..."
|
||||
);
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
|
||||
logging!(info, Type::Setup, true, "Initializing AppHandleManager...");
|
||||
AppHandleManager::global().init(app.handle().clone());
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化核心句柄...");
|
||||
logging!(info, Type::Setup, true, "Initializing core handle...");
|
||||
core::handle::Handle::global().init(app.handle());
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化配置...");
|
||||
logging!(info, Type::Setup, true, "Initializing config...");
|
||||
if let Err(e) = utils::init::init_config() {
|
||||
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to initialize config: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化资源...");
|
||||
logging!(info, Type::Setup, true, "Initializing resources...");
|
||||
if let Err(e) = utils::init::init_resources() {
|
||||
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to initialize resources: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
|
||||
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
// If an early deep link was captured from argv, schedule it now (after core and window can be created)
|
||||
utils::resolve::replay_early_deep_link();
|
||||
|
||||
// (deep link handler already registered above)
|
||||
|
||||
tauri::async_runtime::spawn(async {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"Running profile updates at startup..."
|
||||
);
|
||||
if let Err(e) = crate::cmd::update_profiles_on_startup().await {
|
||||
log::error!("Failed to update profiles on startup: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initialization completed, continuing"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -294,6 +326,8 @@ pub fn run() {
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
cmd::get_next_update_time,
|
||||
cmd::update_profiles_on_startup,
|
||||
cmd::create_profile_from_share_link,
|
||||
// script validation
|
||||
cmd::script_validate_notice,
|
||||
cmd::validate_script_file,
|
||||
@@ -333,7 +367,7 @@ pub fn run() {
|
||||
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||
logging!(info, Type::System, true, "App ready or resumed");
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -341,8 +375,8 @@ pub fn run() {
|
||||
.get_handle()
|
||||
.get_webview_window("main")
|
||||
{
|
||||
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||
let _ = window.set_title("Clash Verge");
|
||||
logging!(info, Type::Window, true, "Setting macOS window title");
|
||||
let _ = window.set_title("Koala Clash");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +387,10 @@ pub fn run() {
|
||||
} => {
|
||||
if !has_visible_windows {
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
}
|
||||
@@ -373,7 +411,6 @@ pub fn run() {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
@@ -382,7 +419,12 @@ pub fn run() {
|
||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"Tried to hide window but it does not exist"
|
||||
);
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Focused(true) => {
|
||||
|
||||
@@ -13,11 +13,17 @@ use crate::AppHandleManager;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Mutex,
|
||||
};
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
|
||||
// 添加退出轻量模式的锁,防止并发调用
|
||||
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn with_lightweight_status<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut LightWeightState) -> R,
|
||||
@@ -30,24 +36,24 @@ where
|
||||
|
||||
pub fn run_once_auto_lightweight() {
|
||||
LightWeightState::default().run_once_time(|| {
|
||||
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
|
||||
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(false);
|
||||
let enable_auto = Config::verge()
|
||||
.data()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(true);
|
||||
.unwrap_or(false);
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"正常创建窗口和添加定时器监听器"
|
||||
"Silent start detected: create window, then attach auto lightweight-mode listener"
|
||||
);
|
||||
set_lightweight_mode(false);
|
||||
disable_auto_light_weight_mode();
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -59,14 +65,19 @@ pub fn auto_lightweight_mode_init() {
|
||||
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(info, Type::Lightweight, true, "自动轻量模式静默启动");
|
||||
if enable_auto && !is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"Non-silent start: directly attach auto lightweight-mode listener"
|
||||
);
|
||||
set_lightweight_mode(true);
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 确保托盘状态更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,39 +89,39 @@ pub fn is_in_lightweight_mode() -> bool {
|
||||
}
|
||||
|
||||
// 设置轻量模式状态
|
||||
fn set_lightweight_mode(value: bool) {
|
||||
pub fn set_lightweight_mode(value: bool) {
|
||||
with_lightweight_status(|state| {
|
||||
state.set_lightweight_mode(value);
|
||||
});
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {}", e);
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
Timer::global().init().unwrap();
|
||||
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||
logging!(info, Type::Lightweight, true, "Enable auto lightweight mode");
|
||||
setup_window_close_listener();
|
||||
setup_webview_focus_listener();
|
||||
}
|
||||
|
||||
pub fn disable_auto_light_weight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
||||
logging!(info, Type::Lightweight, true, "Disable auto lightweight mode");
|
||||
let _ = cancel_light_weight_timer();
|
||||
cancel_window_close_listener();
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
let result = WindowManager::hide_main_window();
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"轻量模式隐藏窗口结果: {:?}",
|
||||
"Lightweight mode window hide result: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
@@ -120,7 +131,6 @@ pub fn entry_lightweight_mode() {
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
}
|
||||
set_lightweight_mode(true);
|
||||
let _ = cancel_light_weight_timer();
|
||||
@@ -131,14 +141,32 @@ pub fn entry_lightweight_mode() {
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
pub fn exit_lightweight_mode() {
|
||||
// 使用原子操作检查是否已经在退出过程中,防止并发调用
|
||||
if EXITING_LIGHTWEIGHT
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"Lightweight mode exit already in progress; skipping duplicate call"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用defer确保无论如何都会重置标志
|
||||
let _guard = scopeguard::guard((), |_| {
|
||||
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// 确保当前确实处于轻量模式才执行退出操作
|
||||
if !is_in_lightweight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||
logging!(info, Type::Lightweight, true, "Not in lightweight mode; skip exit");
|
||||
return;
|
||||
}
|
||||
|
||||
set_lightweight_mode(false);
|
||||
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
||||
|
||||
// macOS激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -164,7 +192,7 @@ fn setup_window_close_listener() -> u32 {
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"监听到关闭请求,开始轻量模式计时"
|
||||
"Close requested; starting lightweight-mode timer"
|
||||
);
|
||||
});
|
||||
return handler;
|
||||
@@ -179,7 +207,7 @@ fn setup_webview_focus_listener() -> u32 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
"监听到窗口获得焦点,取消轻量模式计时"
|
||||
"Window focused; cancel lightweight-mode timer"
|
||||
);
|
||||
});
|
||||
return handler;
|
||||
@@ -190,7 +218,7 @@ fn setup_webview_focus_listener() -> u32 {
|
||||
fn cancel_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
window.unlisten(setup_window_close_listener());
|
||||
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
||||
logging!(info, Type::Lightweight, true, "Removed window close listener");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +243,7 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_once_by_minutes(once_by_minutes)
|
||||
.spawn_async_routine(move || async move {
|
||||
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||
logging!(info, Type::Timer, true, "Timer expired; entering lightweight mode");
|
||||
entry_lightweight_mode();
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
@@ -243,7 +271,7 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
info,
|
||||
Type::Timer,
|
||||
true,
|
||||
"计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||
"Timer set; will auto-enter lightweight mode after {} minute(s)",
|
||||
once_by_minutes
|
||||
);
|
||||
|
||||
@@ -258,7 +286,7 @@ fn cancel_light_weight_timer() -> Result<()> {
|
||||
delay_timer
|
||||
.remove_task(task.task_id)
|
||||
.context("failed to remove timer task")?;
|
||||
logging!(info, Type::Timer, true, "计时器已取消");
|
||||
logging!(info, Type::Timer, true, "Timer canceled");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,8 +4,6 @@ use once_cell::sync::Lazy;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::http::HeaderMap;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::http::HeaderValue;
|
||||
|
||||
// 缓存的最大有效期(5秒)
|
||||
const CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
@@ -99,38 +97,12 @@ impl MihomoManager {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse().unwrap();
|
||||
let secret = format!("Bearer {secret}").parse().unwrap();
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Some((server, headers))
|
||||
}
|
||||
|
||||
// 提供默认值的版本,避免在connection_info为None时panic
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
|
||||
Self::get_clash_client_info().unwrap_or_else(|| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
("http://127.0.0.1:9090".to_string(), headers)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
||||
let (url, headers) = MihomoManager::get_clash_client_info_or_default();
|
||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
||||
let auth = headers
|
||||
.get("Authorization")
|
||||
.map(|val| val.to_str().unwrap_or("").to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 创建默认的空HeaderValue而不是使用unwrap_or_default
|
||||
let token = match HeaderValue::from_str(&auth) {
|
||||
Ok(v) => v,
|
||||
Err(_) => HeaderValue::from_static(""),
|
||||
};
|
||||
|
||||
(ws_url, token)
|
||||
}
|
||||
// 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ impl LightWeightState {
|
||||
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
||||
self.is_lightweight = value;
|
||||
if value {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
logging!(info, Type::Lightweight, true, "Lightweight mode enabled");
|
||||
} else {
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||
logging!(info, Type::Lightweight, true, "Lightweight mode disabled");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ use anyhow::{anyhow, Result};
|
||||
use log::info;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::{fs, path::Path, path::PathBuf};
|
||||
use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
|
||||
|
||||
/// Windows 下的开机启动文件夹路径
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_startup_dir() -> Result<PathBuf> {
|
||||
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("无法获取 APPDATA 环境变量"))?;
|
||||
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("Unable to obtain APPDATA environment variable"))?;
|
||||
|
||||
let startup_dir = Path::new(&appdata)
|
||||
.join("Microsoft")
|
||||
@@ -19,7 +19,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
|
||||
.join("Startup");
|
||||
|
||||
if !startup_dir.exists() {
|
||||
return Err(anyhow!("Startup 目录不存在: {:?}", startup_dir));
|
||||
return Err(anyhow!("Startup directory does not exist: {:?}", startup_dir));
|
||||
}
|
||||
|
||||
Ok(startup_dir)
|
||||
@@ -29,7 +29,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_exe_path() -> Result<PathBuf> {
|
||||
let exe_path =
|
||||
std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?;
|
||||
std::env::current_exe().map_err(|e| anyhow!("Unable to obtain the path of the current executable file: {}", e))?;
|
||||
|
||||
Ok(exe_path)
|
||||
}
|
||||
@@ -39,11 +39,11 @@ pub fn get_exe_path() -> Result<PathBuf> {
|
||||
pub fn create_shortcut() -> Result<()> {
|
||||
let exe_path = get_exe_path()?;
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
|
||||
|
||||
// 如果快捷方式已存在,直接返回成功
|
||||
// If the shortcut already exists, return success directly
|
||||
if shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式已存在");
|
||||
info!(target: "app", "Startup shortcut already exists");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -59,34 +59,36 @@ pub fn create_shortcut() -> Result<()> {
|
||||
|
||||
let output = std::process::Command::new("powershell")
|
||||
.args(["-Command", &powershell_command])
|
||||
// Hide the PowerShell window
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.output()
|
||||
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to execute PowerShell command: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
|
||||
return Err(anyhow!("Failed to create shortcut: {}", error_msg));
|
||||
}
|
||||
|
||||
info!(target: "app", "成功创建启动快捷方式");
|
||||
info!(target: "app", "Successfully created startup shortcut");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除快捷方式
|
||||
/// Remove the shortcut
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn remove_shortcut() -> Result<()> {
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
|
||||
|
||||
// 如果快捷方式不存在,直接返回成功
|
||||
// If the shortcut does not exist, return success directly
|
||||
if !shortcut_path.exists() {
|
||||
info!(target: "app", "启动快捷方式不存在,无需删除");
|
||||
info!(target: "app", "Startup shortcut does not exist, nothing to remove");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 删除快捷方式
|
||||
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
|
||||
// Delete the shortcut
|
||||
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("Failed to delete shortcut: {}", e))?;
|
||||
|
||||
info!(target: "app", "成功删除启动快捷方式");
|
||||
info!(target: "app", "Successfully removed startup shortcut");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -94,7 +96,7 @@ pub fn remove_shortcut() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_shortcut_enabled() -> Result<bool> {
|
||||
let startup_dir = get_startup_dir()?;
|
||||
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
|
||||
|
||||
Ok(shortcut_path.exists())
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ use std::{fs, path::PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
|
||||
pub static APP_ID: &str = "io.github.koala-clash";
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub static BACKUP_DIR: &str = "clash-verge-rev-backup";
|
||||
pub static BACKUP_DIR: &str = "io.github.koala-clash-backup";
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
|
||||
pub static APP_ID: &str = "io.github.koala-clash.dev";
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev";
|
||||
pub static BACKUP_DIR: &str = "io.github.koala-clash-backup-dev";
|
||||
|
||||
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||
|
||||
@@ -94,7 +94,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
|
||||
// 如果无法获取系统目录,则回退到可执行文件目录
|
||||
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
|
||||
log::warn!(target: "app", "Using fallback data directory: {:?}", fallback_dir);
|
||||
log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}");
|
||||
return Ok(fallback_dir);
|
||||
}
|
||||
};
|
||||
@@ -102,7 +102,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
match app_handle.path().data_dir() {
|
||||
Ok(dir) => Ok(dir.join(APP_ID)),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get the app home directory: {}", e);
|
||||
log::error!(target: "app", "Failed to get the app home directory: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
match app_handle.path().resource_dir() {
|
||||
Ok(dir) => Ok(dir.join("resources")),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get the resource directory: {}", e);
|
||||
log::error!(target: "app", "Failed to get the resource directory: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
||||
}
|
||||
}
|
||||
@@ -188,13 +188,13 @@ pub fn profiles_path() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
let res_dir = app_resources_dir()?;
|
||||
Ok(res_dir.join("clash-verge-service"))
|
||||
Ok(res_dir.join("koala-clash-service"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
let res_dir = app_resources_dir()?;
|
||||
Ok(res_dir.join("clash-verge-service.exe"))
|
||||
Ok(res_dir.join("koala-clash-service.exe"))
|
||||
}
|
||||
|
||||
pub fn service_log_file() -> Result<PathBuf> {
|
||||
@@ -203,7 +203,7 @@ pub fn service_log_file() -> Result<PathBuf> {
|
||||
let log_dir = app_logs_dir()?.join("service");
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = format!("{local_time}.log");
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
|
||||
@@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
|
||||
let img = image::load_from_memory(data)?;
|
||||
let rgb_img = img.to_rgb8();
|
||||
|
||||
for pixel in rgb_img.pixels() {
|
||||
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn linux_elevator() -> String {
|
||||
use std::process::Command;
|
||||
@@ -176,39 +163,3 @@ macro_rules! t {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 将字节数转换为可读的流量字符串
|
||||
/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换
|
||||
///
|
||||
/// # Examples
|
||||
/// ```not_run
|
||||
/// format_bytes_speed(1000) // returns "1000B/s"
|
||||
/// format_bytes_speed(1024) // returns "1.0KB/s"
|
||||
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
|
||||
/// ```
|
||||
/// ```
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn format_bytes_speed(speed: u64) -> String {
|
||||
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||
let mut size = speed as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1000.0 && unit_index < UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.1}{}/s", size, UNITS[unit_index])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_format_bytes_speed() {
|
||||
assert_eq!(format_bytes_speed(0), "0.0B/s");
|
||||
assert_eq!(format_bytes_speed(1023), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
|
||||
assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
|
||||
}
|
||||
|
||||