mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 22:00:56 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e93ecc8e | ||
|
|
aeaab10fcb | ||
|
|
cbbd99a748 | ||
|
|
228b013249 | ||
|
|
55fdce58d0 | ||
|
|
c1abb78cf9 | ||
|
|
53023fa189 | ||
|
|
827ab4b1c5 | ||
|
|
2fbcbafc53 | ||
|
|
ab140d6990 | ||
|
|
0e38ed7112 | ||
|
|
5e3ff37602 | ||
|
|
3f84368fc0 | ||
|
|
6ded03c616 | ||
|
|
15fd8b17c3 | ||
|
|
7cb0c8173a | ||
|
|
9b04721b3d | ||
|
|
bc39d56b0f | ||
|
|
a1f671f380 | ||
|
|
147e7d7390 | ||
|
|
e9684fd639 | ||
|
|
d7e9d9498d | ||
|
|
b8161b01c1 | ||
|
|
079591da59 | ||
|
|
1461ab0d37 | ||
|
|
c37a69ea1b | ||
|
|
f78977e267 | ||
|
|
85124549d7 | ||
|
|
90f99a3879 | ||
|
|
8336c43e86 | ||
|
|
dbbd7a1ed7 | ||
|
|
e246cf33da | ||
|
|
22ae6bc583 | ||
|
|
9d13063d53 | ||
|
|
47d581f042 | ||
|
|
6a1587ec73 | ||
|
|
fe891d23d2 | ||
|
|
07c151b287 | ||
|
|
51d61a4bb7 | ||
|
|
37e63ec67c | ||
|
|
106861f761 | ||
|
|
edb9ccdca9 | ||
|
|
6dfa53fc23 | ||
|
|
abe0750d07 | ||
|
|
2195576553 | ||
|
|
1dc015f411 | ||
|
|
49d13c9ecb | ||
|
|
d8cd91b725 | ||
|
|
40eede325b | ||
|
|
59e07a3e55 | ||
|
|
3d32d2d08f | ||
|
|
46af096cb1 | ||
|
|
486f33fd7a | ||
|
|
5ebfcc6f98 | ||
|
|
7fa1767645 | ||
|
|
33d54b27f4 | ||
|
|
f8dbc7bea0 | ||
|
|
83d338eaa4 | ||
|
|
9732b2be14 | ||
|
|
8a5232df20 | ||
|
|
5f0f6db0d5 | ||
|
|
7a86e4bd85 | ||
|
|
f18a8c901f | ||
|
|
23d161c298 | ||
|
|
3e2148e236 | ||
|
|
70a4bd1074 | ||
|
|
e13d3e7f83 | ||
|
|
e21bc22fa3 | ||
|
|
d8e85c9cfc | ||
|
|
1d5471e70d | ||
|
|
a2a8dd182a | ||
|
|
511fb9e2cf | ||
|
|
14f90ac925 | ||
|
|
8e09bf525e | ||
|
|
7e7ef5766e | ||
|
|
b078560ac1 | ||
|
|
1e481ed1c1 | ||
|
|
e957c10138 | ||
|
|
79bb0f29f9 | ||
|
|
f4cb978118 | ||
|
|
e16b23946a | ||
|
|
df538ca9a3 | ||
|
|
b97f0734d0 | ||
|
|
cbd2dd972a | ||
|
|
00d6bcc00f | ||
|
|
43ffb3c9e0 | ||
|
|
9fd75f0efb | ||
|
|
49f0ac407e | ||
|
|
ff8426d95e | ||
|
|
3ca4ac2726 | ||
|
|
07441ba95e | ||
|
|
e74fd068d5 | ||
|
|
965e281348 | ||
|
|
a7f66c3d65 | ||
|
|
f7f15544ba | ||
|
|
6ea79daf73 | ||
|
|
99abbfbc4c | ||
|
|
24df5e02d6 | ||
|
|
6aaec3de5d | ||
|
|
39d811cc62 | ||
|
|
832a0ac281 | ||
|
|
7b0df043be | ||
|
|
d470bf2dd9 | ||
|
|
b5e8a1ddb5 | ||
|
|
08f3357d9a | ||
|
|
107c54921d | ||
|
|
ca1f7a2b31 | ||
|
|
95e8fe38f2 | ||
|
|
cd6eb26a41 | ||
|
|
9f2c4b4f35 | ||
|
|
adbf98cb15 | ||
|
|
c9b3fec09c | ||
|
|
364fb16b7b | ||
|
|
634ed9cb46 | ||
|
|
cddb117402 | ||
|
|
bd6c4e00c4 | ||
|
|
bbcfb4901f | ||
|
|
230a0eeb64 | ||
|
|
c4c49f61df | ||
|
|
6d5c4f86d3 | ||
|
|
59f22b9cd2 | ||
|
|
6caf2cd057 | ||
|
|
684128ca68 | ||
|
|
6925adc66f | ||
|
|
e1e9d91d86 | ||
|
|
881c136b1e | ||
|
|
5bc699b19c | ||
|
|
52ba1c5a30 | ||
|
|
817d68546e | ||
|
|
c0f5c231ad | ||
|
|
9677d8670b | ||
|
|
b01630d31c | ||
|
|
f771f4720f | ||
|
|
f0becb5003 | ||
|
|
405e8df825 | ||
|
|
49fc7ff7ca | ||
|
|
fde9c8aaee | ||
|
|
8f95c28050 | ||
|
|
2cd97c7785 | ||
|
|
6e48781687 | ||
|
|
dd510b2ee9 | ||
|
|
fadae3f7dc | ||
|
|
fdf8a6b2ba | ||
|
|
4382df7951 | ||
|
|
6cb7d48530 | ||
|
|
9d74b93ee0 | ||
|
|
d370868222 | ||
|
|
9278f9e193 | ||
|
|
a708db6f2b | ||
|
|
d2c4a44297 | ||
|
|
3a24623e4b | ||
|
|
d2084d8d21 | ||
|
|
d9b4ca91e7 | ||
|
|
13da571ad2 | ||
|
|
662d60c0f5 | ||
|
|
368095d2d4 | ||
|
|
496aeeb06d | ||
|
|
b31cbda615 | ||
|
|
7898f3a119 | ||
|
|
ba0a291d97 | ||
|
|
b3e4defc0f | ||
|
|
1ae0ad8a0e | ||
|
|
79b20694c7 | ||
|
|
1ad4941ed8 | ||
|
|
b3fd44d165 | ||
|
|
391a494af6 | ||
|
|
623d075ab8 | ||
|
|
5d70b77316 | ||
|
|
f5f54c0f0e | ||
|
|
91b0b1f279 | ||
|
|
e0fa1f3efe | ||
|
|
b00abf3337 | ||
|
|
a5d846ce4e | ||
|
|
7743c42dd1 | ||
|
|
3f3dab9495 | ||
|
|
2dc9672b20 | ||
|
|
2c90d1ca69 | ||
|
|
1555e2910d | ||
|
|
19b6bd35f5 | ||
|
|
3d4d60edc8 | ||
|
|
db0a000ecb | ||
|
|
24ff48b41c | ||
|
|
18a67d0b2f | ||
|
|
e23af1ad58 | ||
|
|
70af392059 | ||
|
|
d255df23ee | ||
|
|
1317a8b2db | ||
|
|
4cac118442 |
@@ -2,6 +2,7 @@ name: 问题反馈 / Bug report
|
||||
title: "[BUG] "
|
||||
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||
labels: ["bug"]
|
||||
type: "Bug"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -11,14 +12,16 @@ body:
|
||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
||||
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
|
||||
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
|
||||
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
|
||||
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
|
||||
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
|
||||
@@ -2,6 +2,7 @@ name: 功能请求 / Feature request
|
||||
title: "[Feature] "
|
||||
description: 提出你的功能请求 / Propose your feature request
|
||||
labels: ["enhancement"]
|
||||
type: "Feature"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -33,3 +34,14 @@ body:
|
||||
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: os-labels
|
||||
attributes:
|
||||
label: 适用系统 / Target OS
|
||||
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
|
||||
options:
|
||||
- label: windows
|
||||
- label: macos
|
||||
- label: linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
+195
-76
@@ -2,6 +2,9 @@ name: Alpha Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 0,6,12,18
|
||||
- cron: "0 16,22,4,10 * * *"
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@@ -12,7 +15,184 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
check_commit:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed
|
||||
id: check
|
||||
run: |
|
||||
# For manual workflow_dispatch, always run
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Store current version from package.json
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Get the previous commit's package.json version
|
||||
git checkout HEAD~1 package.json
|
||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Previous version: $PREVIOUS_VERSION"
|
||||
|
||||
# Reset back to current commit
|
||||
git checkout HEAD package.json
|
||||
|
||||
# Check if version changed
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version unchanged: $CURRENT_VERSION"
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
delete_old_assets:
|
||||
needs: check_commit
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Release Assets
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const releaseTag = 'alpha';
|
||||
|
||||
try {
|
||||
// Get the release by tag name
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag: releaseTag
|
||||
});
|
||||
|
||||
console.log(`Found release with ID: ${release.id}`);
|
||||
|
||||
// Delete each asset
|
||||
if (release.assets && release.assets.length > 0) {
|
||||
console.log(`Deleting ${release.assets.length} assets`);
|
||||
|
||||
for (const asset of release.assets) {
|
||||
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
asset_id: asset.id
|
||||
});
|
||||
}
|
||||
|
||||
console.log('All assets deleted successfully');
|
||||
} else {
|
||||
console.log('No assets found to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log('Release not found, nothing to delete');
|
||||
} else {
|
||||
console.error('Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: delete_old_assets
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch Alpha update logs
|
||||
id: fetch_alpha_logs
|
||||
run: |
|
||||
# Check if UPDATELOG.md exists
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
# Extract the section starting with ## and containing -alpha until the next ## or end of file
|
||||
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
|
||||
if [ -n "$ALPHA_LOGS" ]; then
|
||||
echo "Found alpha update logs"
|
||||
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$ALPHA_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No alpha sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息
|
||||
if [ -z "$ALPHA_LOGS" ]; then
|
||||
echo "No alpha logs found, using default message"
|
||||
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found alpha logs"
|
||||
fi
|
||||
|
||||
# 生成 release.txt 文件
|
||||
cat > release.txt << EOF
|
||||
$ALPHA_LOGS
|
||||
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: 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)
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
alpha:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -50,7 +230,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -67,8 +247,8 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Alpha Version update
|
||||
run: pnpm run fix-alpha-version
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@@ -92,13 +272,8 @@ jobs:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm portable ${{ matrix.target }} --alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
alpha-for-linux-arm:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -141,6 +316,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
sudo ls -lR /etc/apt/
|
||||
@@ -164,6 +342,7 @@ jobs:
|
||||
sudo apt update
|
||||
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
@@ -203,7 +382,7 @@ jobs:
|
||||
- name: Get Version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
|
||||
@@ -212,7 +391,6 @@ jobs:
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
@@ -220,6 +398,7 @@ jobs:
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
alpha-for-fixed-webview2:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -260,6 +439,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- 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
|
||||
@@ -293,9 +475,9 @@ jobs:
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
@@ -304,7 +486,6 @@ jobs:
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
@@ -313,65 +494,3 @@ jobs:
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: [alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# - name: Update Tag
|
||||
# uses: richardsimko/update-tag@v1
|
||||
# with:
|
||||
# tag_name: alpha
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: |
|
||||
cat > release.txt << 'EOF'
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
|
||||
### Windows (Win7 用户请查看下面FAQ中的解决方案)
|
||||
#### 正常版本(推荐)
|
||||
- 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)
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -6,6 +6,10 @@ permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
concurrency:
|
||||
# only allow per workflow per commit (and not pr) to run at a time
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -40,12 +44,13 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -82,12 +87,6 @@ jobs:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm portable ${{ matrix.target }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-for-linux-arm:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -154,6 +153,7 @@ jobs:
|
||||
sudo apt update
|
||||
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
@@ -216,9 +216,6 @@ jobs:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
arch: x64
|
||||
- os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
arch: x86
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
arch: arm64
|
||||
@@ -234,6 +231,8 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -283,9 +282,9 @@ jobs:
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -1 +1,16 @@
|
||||
pnpm pretty-quick --staged
|
||||
#!/bin/bash
|
||||
|
||||
#pnpm pretty-quick --staged
|
||||
|
||||
# 运行 clippy fmt
|
||||
#cargo fmt --manifest-path ./src-tauri/Cargo.toml
|
||||
|
||||
# if [ $? -ne 0 ]; then
|
||||
# echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
#git add .
|
||||
|
||||
# 允许提交
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 运行 clippy
|
||||
#cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||
|
||||
# 如果 clippy 失败,阻止 push
|
||||
#if [ $? -ne 0 ]; then
|
||||
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||
# exit 1
|
||||
#fi
|
||||
|
||||
# 允许 push
|
||||
exit 0
|
||||
+27
-3
@@ -38,7 +38,7 @@ npm install pnpm -g
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Download the Clash Mihomo Core Binary
|
||||
### Download the Mihomo Core Binary
|
||||
|
||||
You have two options for downloading the clash binary:
|
||||
|
||||
@@ -48,7 +48,7 @@ You have two options for downloading the clash binary:
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --force
|
||||
```
|
||||
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
### Run the Development Server
|
||||
|
||||
@@ -65,11 +65,35 @@ pnpm dev:diff
|
||||
To build this project:
|
||||
|
||||
```shell
|
||||
pnpm run build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
For a faster build, use the following command
|
||||
|
||||
```shell
|
||||
pnpm build:fast
|
||||
```
|
||||
|
||||
This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly.
|
||||
|
||||
The `Artifacts` will display in the `log` in the Terminal.
|
||||
|
||||
### Build clean
|
||||
|
||||
To clean rust build:
|
||||
|
||||
```shell
|
||||
pnpm clean
|
||||
```
|
||||
|
||||
### Portable Version (Windows Only)
|
||||
|
||||
To package portable version after the build:
|
||||
|
||||
```shell
|
||||
pnpm portable
|
||||
```
|
||||
|
||||
## Contributing Your Changes
|
||||
|
||||
Once you have made your changes:
|
||||
|
||||
+125
-2
@@ -1,4 +1,113 @@
|
||||
## v2.1.1
|
||||
## v2.2.1
|
||||
|
||||
**发行代号:拓**
|
||||
|
||||
感谢 Tunglies 对 Verge 后端重构,性能优化做出的重大贡献!
|
||||
|
||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||
|
||||
2.2.1 相对于 2.2.0(已下架不在提供)
|
||||
修复了:
|
||||
1. **首页**
|
||||
- 修复 Direct 模式首页无法渲染
|
||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||
- 修复 系统代理标识判断不准的问题
|
||||
- 修复 系统代理地址错误的问题
|
||||
- 代理模式“多余的切换动画”
|
||||
2. **系统**
|
||||
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
|
||||
- 修复 代理端口设置同步问题。
|
||||
- 修复 Linux 无法与 Mihomo 核心 和 ClashVergeRev 服务通信
|
||||
3. **界面**
|
||||
- 修复 连接详情卡没有跟随主题色
|
||||
4. **轻量模式**
|
||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||
|
||||
新增了:
|
||||
1. **首页**
|
||||
- 首页文本过长自动截断
|
||||
2. **轻量模式**
|
||||
- 新增托盘进入轻量模式支持
|
||||
- 新增进入轻量模式快捷键支持
|
||||
3. **系统**
|
||||
- 在 ClashVergeRev 对 Mihomo 进行操作时,总是尝试确保两者运行
|
||||
- 服务器模式下启动mihomo内核的时候查找并停止其他已经存在的内核进程,防止内核假死等问题带来的通信失败
|
||||
4. **托盘**
|
||||
- 新增 MacOS 启用托盘速率显示时,可选隐藏托盘图标显示
|
||||
|
||||
---
|
||||
|
||||
## v2.2.0(已下架不在提供)
|
||||
|
||||
#### 新增功能
|
||||
1. **首页**
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
- 首页流量图卡片显示上传/下载名称。
|
||||
- 首页支持轻量模式切换。
|
||||
- 流量统计数据持久保存。
|
||||
- 限制首页配置文件卡片URL长度。
|
||||
|
||||
2. **DNS 设置与覆写**
|
||||
- 新增 DNS 覆写功能。
|
||||
- 默认启用 DNS 覆写。
|
||||
|
||||
3. **解锁测试**
|
||||
- 新增解锁测试页面。
|
||||
|
||||
4. **轻量模式**
|
||||
- 新增轻量模式及设置。
|
||||
- 添加自动轻量模式定时器。
|
||||
|
||||
5. **系统支持**
|
||||
- Mihomo(meta)内核升级 1.19.3
|
||||
- macOS 支持 CMD+W 关闭窗口。
|
||||
- 新增 macOS 应用菜单。
|
||||
- 添加 macOS 安装服务时候的管理员权限提示。
|
||||
- 新增 sidecar(用户空间启动内核) 模式。
|
||||
|
||||
6. **其他**
|
||||
- 增强延迟测试日志和错误处理。
|
||||
- 添加诊断信息导出。
|
||||
- 新增代理命令。
|
||||
|
||||
#### 修复
|
||||
1. **系统**
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
- 修复 macOS 静默启动崩溃。
|
||||
- 修复 macOS tray图标错位到左上角的问题。
|
||||
- 修复 Windows/Linux 运行时崩溃。
|
||||
- 修复 Win10 阴影和边框问题。
|
||||
|
||||
2. **构建**
|
||||
- 修复构建失败问题。
|
||||
|
||||
#### 优化
|
||||
1. **性能**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
- 优化流量图表资源使用。
|
||||
- 提升代理组列表滚动性能。
|
||||
- 加快应用退出速度。
|
||||
- 加快进入轻量模式速度。
|
||||
- 优化小数值速度更新。
|
||||
- 增加请求超时至 60 秒。
|
||||
- 修复代理节点选择同步。
|
||||
- 优化修改verge配置性能。
|
||||
|
||||
2. **重构**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化定时器管理。
|
||||
- 重构 MihomoManager 处理流量。
|
||||
- 优化 WebSocket 连接。
|
||||
|
||||
3. **其他**
|
||||
- 更新依赖。
|
||||
- 默认 TUN 堆栈改为 gvisor。
|
||||
|
||||
---
|
||||
|
||||
## v2.1.2
|
||||
|
||||
**发行代号:臻**
|
||||
|
||||
@@ -6,8 +115,16 @@
|
||||
|
||||
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
||||
|
||||
2.1.1相对2.1.0(已下架不在提供)更新了:
|
||||
##### 2.1.2相对2.1.1(已下架不在提供)更新了:
|
||||
|
||||
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
||||
- 设置菜单区分Verge基本设置和高级设置
|
||||
- 增加v2 Updater的更多功能和权限
|
||||
- 退出Verge后Tun代理状态仍保留的问题
|
||||
|
||||
##### 2.1.1相对2.1.0(已下架不在提供)更新了:
|
||||
|
||||
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
||||
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
||||
- 文件类型判断不准导致脚本检测报错的问题
|
||||
- 打开Win下的阴影(Win10因底层兼容性问题,可能圆角和边框显示不太完美)
|
||||
@@ -15,6 +132,8 @@
|
||||
- 修复Linux下编译问题
|
||||
- 修复热键无法关闭面板的问题
|
||||
|
||||
##### 2.1.0 - 发行代号:臻
|
||||
|
||||
### 功能新增
|
||||
|
||||
- 新增窗口状态实时监控与自动保存功能
|
||||
@@ -91,6 +210,8 @@
|
||||
- 改进脚本验证与异常处理流程
|
||||
- 修复编译警告(移除无用导入)
|
||||
|
||||
---
|
||||
|
||||
## v2.0.3
|
||||
|
||||
### Notice
|
||||
@@ -138,6 +259,8 @@
|
||||
- 改进更新托盘图标性能
|
||||
- 窗口隐藏后WebSocket断开连接
|
||||
|
||||
---
|
||||
|
||||
## v2.0.2
|
||||
|
||||
### Notice
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 314 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 274 KiB |
+36
-30
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc --noEmit && vite build",
|
||||
@@ -15,8 +16,10 @@
|
||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
||||
"prepare": "husky"
|
||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||
"prepare": "husky",
|
||||
"clean": "cd ./src-tauri && cargo clean && cd -"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -25,46 +28,49 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^6.4.2",
|
||||
"@mui/icons-material": "^6.4.8",
|
||||
"@mui/lab": "6.0.0-beta.25",
|
||||
"@mui/material": "^6.4.2",
|
||||
"@mui/x-data-grid": "^7.25.0",
|
||||
"@mui/material": "^6.4.8",
|
||||
"@mui/x-data-grid": "^7.28.0",
|
||||
"@tauri-apps/api": "2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.1",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.3.0",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.8.3",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.43",
|
||||
"foxact": "^0.2.44",
|
||||
"glob": "^11.0.1",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next": "^24.2.3",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.2.3",
|
||||
"nanoid": "^5.0.9",
|
||||
"monaco-yaml": "^5.3.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^4.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-monaco-editor": "^0.56.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-monaco-editor": "^0.56.2",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.6.3",
|
||||
"react-virtuoso": "^4.12.5",
|
||||
"recharts": "^2.15.1",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.0",
|
||||
"swr": "^2.3.3",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.3"
|
||||
@@ -78,20 +84,20 @@
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitejs/plugin-legacy": "^6.0.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.1",
|
||||
"meta-json-schema": "^1.19.3",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.4.2",
|
||||
"pretty-quick": "^4.0.0",
|
||||
"sass": "^1.83.4",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
|
||||
Generated
+3010
-4598
File diff suppressed because it is too large
Load Diff
@@ -33,22 +33,24 @@ async function updatePackageVersion(newVersion) {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
// 读取文件
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
const initversion = packageJson.version;
|
||||
// 将匹配到的第一个 "alpha" => 具体的hash
|
||||
const fixversion = initversion.replace("alpha", newVersion);
|
||||
packageJson.version = fixversion;
|
||||
// 获取键值替换
|
||||
let result = packageJson.version.replace("alpha", newVersion);
|
||||
console.log("[INFO]: Current version is: ", result);
|
||||
packageJson.version = result;
|
||||
// 写入版本号
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`Alpha version update to: ${fixversion}`);
|
||||
console.log(`[INFO]: Alpha version update to: ${newVersion}`);
|
||||
} catch (error) {
|
||||
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||
}
|
||||
}
|
||||
|
||||
const newVersion = await getLatestCommitHash();
|
||||
updatePackageVersion(newVersion);
|
||||
updatePackageVersion(newVersion).catch(console.error);
|
||||
+2
-44
@@ -2,20 +2,16 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import fsp from "fs/promises";
|
||||
|
||||
const target = process.argv.slice(2)[0];
|
||||
const alpha = process.argv.slice(2)[1];
|
||||
|
||||
const ARCH_MAP = {
|
||||
"x86_64-pc-windows-msvc": "x64",
|
||||
"i686-pc-windows-msvc": "x86",
|
||||
"aarch64-pc-windows-msvc": "arm64",
|
||||
};
|
||||
|
||||
const PROCESS_MAP = {
|
||||
x64: "x64",
|
||||
ia32: "x86",
|
||||
arm64: "arm64",
|
||||
};
|
||||
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
|
||||
@@ -37,10 +33,9 @@ async function resolvePortable() {
|
||||
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
|
||||
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
|
||||
}
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
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.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
@@ -49,46 +44,9 @@ async function resolvePortable() {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require("../package.json");
|
||||
const { version } = packageJson;
|
||||
|
||||
const zipFile = `Clash.Verge_${version}_${arch}_portable.zip`;
|
||||
zip.writeZip(zipFile);
|
||||
|
||||
console.log("[INFO]: create portable zip successfully");
|
||||
|
||||
// push release assets
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
throw new Error("GITHUB_TOKEN is required");
|
||||
}
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
|
||||
console.log("[INFO]: upload to ", tag);
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag,
|
||||
});
|
||||
|
||||
let assets = release.assets.filter((x) => {
|
||||
return x.name === zipFile;
|
||||
});
|
||||
if (assets.length > 0) {
|
||||
let id = assets[0].id;
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: id,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(release.name);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: release.id,
|
||||
name: zipFile,
|
||||
data: zip.toBuffer(),
|
||||
});
|
||||
}
|
||||
|
||||
resolvePortable().catch(console.error);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 更新 package.json 文件中的版本号
|
||||
*/
|
||||
async function updatePackageVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
let result = packageJson.version;
|
||||
if (!result.includes("alpha")) {
|
||||
result = `${result}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current package.json version is: ", result);
|
||||
packageJson.version = result;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: package.json version updated to: ${result}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Cargo.toml 文件中的版本号
|
||||
*/
|
||||
async function updateCargoVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.startsWith("version =")) {
|
||||
const versionMatch = line.match(/version\s*=\s*"([^"]+)"/);
|
||||
if (versionMatch && !versionMatch[1].includes("alpha")) {
|
||||
const newVersion = `${versionMatch[1]}-alpha`;
|
||||
return line.replace(versionMatch[1], newVersion);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 tauri.conf.json 文件中的版本号
|
||||
*/
|
||||
async function updateTauriConfigVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
|
||||
let version = tauriConfig.version;
|
||||
if (!version.includes("alpha")) {
|
||||
version = `${version}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current tauri.conf.json version is: ", version);
|
||||
tauriConfig.version = version;
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: tauri.conf.json version updated to: ${version}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数,依次更新所有文件的版本号
|
||||
*/
|
||||
async function main() {
|
||||
await updatePackageVersion();
|
||||
await updateCargoVersion();
|
||||
await updateTauriConfigVersion();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -43,3 +43,42 @@ export async function resolveUpdateLog(tag) {
|
||||
|
||||
return map[tag].join("\n").trim();
|
||||
}
|
||||
|
||||
export async function resolveUpdateLogDefault() {
|
||||
const cwd = process.cwd();
|
||||
const file = path.join(cwd, UPDATE_LOG);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error("could not found UPDATELOG.md");
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(file, "utf-8");
|
||||
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
let content = [];
|
||||
let firstTag = "";
|
||||
|
||||
for (const line of data.split("\n")) {
|
||||
if (reTitle.test(line) && !isCapturing) {
|
||||
isCapturing = true;
|
||||
firstTag = line.slice(3).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCapturing) {
|
||||
if (reEnd.test(line)) {
|
||||
break;
|
||||
}
|
||||
content.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstTag) {
|
||||
throw new Error("could not found any version tag in UPDATELOG.md");
|
||||
}
|
||||
|
||||
return content.join("\n").trim();
|
||||
}
|
||||
|
||||
+154
-49
@@ -1,10 +1,15 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
|
||||
// Add stable update JSON filenames
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
const UPDATE_JSON_FILE = "update.json";
|
||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
// Add alpha update JSON filenames
|
||||
const ALPHA_TAG_NAME = "updater-alpha";
|
||||
const ALPHA_UPDATE_JSON_FILE = "update-alpha.json";
|
||||
const ALPHA_UPDATE_JSON_PROXY = "update-alpha-proxy.json";
|
||||
|
||||
/// generate update.json
|
||||
/// upload to update tag's release asset
|
||||
@@ -16,26 +21,73 @@ async function resolveUpdater() {
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { data: tags } = await github.rest.repos.listTags({
|
||||
...options,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
// Fetch all tags using pagination
|
||||
let allTags = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
|
||||
// get the latest publish tag
|
||||
const tag = tags.find((t) => t.name.startsWith("v"));
|
||||
while (true) {
|
||||
const { data: pageTags } = await github.rest.repos.listTags({
|
||||
...options,
|
||||
per_page: perPage,
|
||||
page: page,
|
||||
});
|
||||
|
||||
console.log(tag);
|
||||
allTags = allTags.concat(pageTags);
|
||||
|
||||
// Break if we received fewer tags than requested (last page)
|
||||
if (pageTags.length < perPage) {
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
const tags = allTags;
|
||||
console.log(`Retrieved ${tags.length} tags in total`);
|
||||
|
||||
// More flexible tag detection with regex patterns
|
||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||
|
||||
// Get the latest stable tag and pre-release tag
|
||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||
|
||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||
console.log(
|
||||
"Pre-release tag:",
|
||||
preReleaseTag ? preReleaseTag.name : "None found",
|
||||
);
|
||||
console.log();
|
||||
|
||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
||||
// Process stable release
|
||||
if (stableTag) {
|
||||
await processRelease(github, options, stableTag, false);
|
||||
}
|
||||
|
||||
// Process pre-release if found
|
||||
if (preReleaseTag) {
|
||||
await processRelease(github, options, preReleaseTag, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Process a release (stable or alpha) and generate update files
|
||||
async function processRelease(github, options, tag, isAlpha) {
|
||||
if (!tag) return;
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tag.name,
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||
),
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
win64: { signature: "", url: "" }, // compatible with older formats
|
||||
@@ -56,39 +108,40 @@ async function resolveUpdater() {
|
||||
},
|
||||
};
|
||||
|
||||
const promises = latestRelease.assets.map(async (asset) => {
|
||||
const promises = release.assets.map(async (asset) => {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
// Process all the platform URL and signature data
|
||||
// win64 url
|
||||
if (name.endsWith("x64-setup.nsis.zip")) {
|
||||
if (name.endsWith("x64-setup.exe")) {
|
||||
updateData.platforms.win64.url = browser_download_url;
|
||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// win64 signature
|
||||
if (name.endsWith("x64-setup.nsis.zip.sig")) {
|
||||
if (name.endsWith("x64-setup.exe.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.win64.signature = sig;
|
||||
updateData.platforms["windows-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// win32 url
|
||||
if (name.endsWith("x86-setup.nsis.zip")) {
|
||||
if (name.endsWith("x86-setup.exe")) {
|
||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||
}
|
||||
// win32 signature
|
||||
if (name.endsWith("x86-setup.nsis.zip.sig")) {
|
||||
if (name.endsWith("x86-setup.exe.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["windows-x86"].signature = sig;
|
||||
updateData.platforms["windows-i686"].signature = sig;
|
||||
}
|
||||
|
||||
// win arm url
|
||||
if (name.endsWith("arm64-setup.nsis.zip")) {
|
||||
if (name.endsWith("arm64-setup.exe")) {
|
||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||
}
|
||||
// win arm signature
|
||||
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
|
||||
if (name.endsWith("arm64-setup.exe.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["windows-aarch64"].signature = sig;
|
||||
}
|
||||
@@ -143,8 +196,7 @@ async function resolveUpdater() {
|
||||
}
|
||||
});
|
||||
|
||||
// 生成一个代理github的更新文件
|
||||
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
||||
// Generate a proxy update file for accelerated GitHub resources
|
||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
@@ -156,42 +208,95 @@ async function resolveUpdater() {
|
||||
}
|
||||
});
|
||||
|
||||
// update the update.json
|
||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: UPDATE_TAG_NAME,
|
||||
});
|
||||
// Get the appropriate updater release based on isAlpha flag
|
||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||
console.log(
|
||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
releaseTag,
|
||||
);
|
||||
|
||||
// delete the old assets
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === UPDATE_JSON_FILE) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
try {
|
||||
let updateRelease;
|
||||
|
||||
try {
|
||||
// Try to get the existing release
|
||||
const response = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
asset_id: asset.id,
|
||||
tag: releaseTag,
|
||||
});
|
||||
updateRelease = response.data;
|
||||
console.log(
|
||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
// If release doesn't exist, create it
|
||||
if (error.status === 404) {
|
||||
console.log(
|
||||
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||
);
|
||||
const createResponse = await github.rest.repos.createRelease({
|
||||
...options,
|
||||
tag_name: releaseTag,
|
||||
name: isAlpha
|
||||
? "Auto-update Alpha Channel"
|
||||
: "Auto-update Stable Channel",
|
||||
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
||||
prerelease: isAlpha,
|
||||
});
|
||||
updateRelease = createResponse.data;
|
||||
console.log(
|
||||
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||
);
|
||||
} else {
|
||||
// If it's another error, throw it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (asset.name === UPDATE_JSON_PROXY) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
// File names based on release type
|
||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||
|
||||
// Delete existing assets with these names
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === jsonFile) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: asset.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.name === proxyFile) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
// Upload new assets
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: jsonFile,
|
||||
data: JSON.stringify(updateData, null, 2),
|
||||
});
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: proxyFile,
|
||||
data: JSON.stringify(updateDataNew, null, 2),
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
// upload new assets
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_FILE,
|
||||
data: JSON.stringify(updateData, null, 2),
|
||||
});
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_PROXY,
|
||||
data: JSON.stringify(updateDataNew, null, 2),
|
||||
});
|
||||
}
|
||||
|
||||
// get the signature file content
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
avoid-breaking-exported-api = true
|
||||
Generated
+1572
-784
File diff suppressed because it is too large
Load Diff
+46
-14
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.1.1"
|
||||
version = "2.2.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -13,36 +13,38 @@ build = "build.rs"
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.5", features = [] }
|
||||
tauri-build = { version = "2.0.6", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
anyhow = "1.0"
|
||||
anyhow = "1.0.97"
|
||||
dirs = "6.0"
|
||||
open = "5.1"
|
||||
log = "0.4"
|
||||
dunce = "1.0"
|
||||
log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
chrono = "0.4.40"
|
||||
sysinfo = "0.33.1"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
once_cell = "1.19"
|
||||
once_cell = "1.20.3"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
fs2 = "0.4.3"
|
||||
window-shadows = { version = "0.2.2" }
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.10.5"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||
image = "0.24"
|
||||
imageproc = "0.23"
|
||||
image = "0.25.5"
|
||||
imageproc = "0.25.0"
|
||||
rusttype = "0.9"
|
||||
tauri = { version = "2.2.5", features = [
|
||||
tauri = { version = "2.3.1", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -59,14 +61,19 @@ tauri-plugin-clipboard-manager = "2.2.1"
|
||||
tauri-plugin-deep-link = "2.2.0"
|
||||
tauri-plugin-devtools = "2.0.0-rc"
|
||||
url = "2.5.4"
|
||||
zip = "2.2.2"
|
||||
zip = "2.2.3"
|
||||
reqwest_dav = "0.1.14"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.2"
|
||||
tokio-tungstenite = "0.26.1"
|
||||
getrandom = "0.3.1"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
futures = "0.3"
|
||||
sys-locale = "0.3.1"
|
||||
async-trait = "0.1.87"
|
||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||
ab_glyph = "0.2.29"
|
||||
tungstenite = "0.26.2"
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -74,14 +81,13 @@ deelevate = "0.2.0"
|
||||
winreg = "0.55.0"
|
||||
url = "2.5.4"
|
||||
|
||||
|
||||
[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.2.0"
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-updater = "2.3.0"
|
||||
tauri-plugin-updater = "2.5.1"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
#openssl
|
||||
|
||||
@@ -100,6 +106,32 @@ strip = true
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
|
||||
[profile.fast-release]
|
||||
inherits = "release" # 继承 release 的配置
|
||||
panic = "abort" # 与 release 相同
|
||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||
lto = false # 禁用 LTO,提升编译速度
|
||||
opt-level = 0 # 禁用优化,大幅提升编译速度
|
||||
debug = true # 保留调试信息
|
||||
strip = false # 不剥离符号,保留调试信息
|
||||
|
||||
[profile.fast-dev]
|
||||
inherits = "dev" # 继承 dev 的配置
|
||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||
incremental = true # 启用增量编译
|
||||
debug = true # 保留调试信息
|
||||
strip = false # 不剥离符号,保留调试信息
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11.0"
|
||||
mockito = "1.7.0"
|
||||
tempfile = "3.17.1"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"src_crates/crate_mihomo_api",]
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
"permissions": [
|
||||
"global-shortcut:default",
|
||||
"updater:default",
|
||||
"dialog:default",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-message",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download-and-install",
|
||||
"process:allow-restart",
|
||||
"deep-link:default",
|
||||
"window-state:default",
|
||||
"window-state:default",
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
use super::CmdResult;
|
||||
use crate::{feat, utils::dirs, wrap_err};
|
||||
use tauri::Manager;
|
||||
|
||||
/// 打开应用程序所在目录
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// 打开核心所在目录
|
||||
#[tauri::command]
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
/// 打开日志目录
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
/// 打开网页链接
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if !window.is_devtools_open() {
|
||||
window.open_devtools();
|
||||
} else {
|
||||
window.close_devtools();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 退出应用
|
||||
#[tauri::command]
|
||||
pub fn exit_app() {
|
||||
feat::quit(Some(0));
|
||||
}
|
||||
|
||||
/// 重启应用
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取便携版标识
|
||||
#[tauri::command]
|
||||
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
}
|
||||
|
||||
/// 获取应用目录
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
/// 获取当前自启动状态
|
||||
#[tauri::command]
|
||||
pub fn get_auto_launch_status() -> CmdResult<bool> {
|
||||
use crate::core::sysopt::Sysopt;
|
||||
wrap_err!(Sysopt::global().get_launch_status())
|
||||
}
|
||||
|
||||
/// 下载图标缓存
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||
let icon_path = icon_cache_dir.join(&name);
|
||||
|
||||
// 如果文件已存在,直接返回路径
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// 确保缓存目录存在
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
}
|
||||
|
||||
// 使用临时文件名来下载
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||
|
||||
// 下载文件到临时位置
|
||||
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||
|
||||
// 检查内容类型是否为图片
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let is_image = content_type.starts_with("image/");
|
||||
|
||||
// 获取响应内容
|
||||
let content = wrap_err!(response.bytes().await)?;
|
||||
|
||||
// 检查内容是否为HTML (针对CDN错误页面)
|
||||
let is_html = content.len() > 15
|
||||
&& (content.starts_with(b"<!DOCTYPE html")
|
||||
|| content.starts_with(b"<html")
|
||||
|| content.starts_with(b"<?xml"));
|
||||
|
||||
// 只有当内容确实是图片时才保存
|
||||
if is_image && !is_html {
|
||||
{
|
||||
let mut file = match std::fs::File::create(&temp_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
} else {
|
||||
return Err("Failed to create temporary file".into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||
}
|
||||
|
||||
// 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件
|
||||
if !icon_path.exists() {
|
||||
match std::fs::rename(&temp_path, &icon_path) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
}
|
||||
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {}", url))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct IconInfo {
|
||||
name: String,
|
||||
previous_t: String,
|
||||
current_t: String,
|
||||
}
|
||||
|
||||
/// 复制图标文件
|
||||
#[tauri::command]
|
||||
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||
if !icon_dir.exists() {
|
||||
let _ = fs::create_dir_all(&icon_dir);
|
||||
}
|
||||
let ext = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().to_string(),
|
||||
None => "ico".to_string(),
|
||||
};
|
||||
|
||||
let dest_path = icon_dir.join(format!(
|
||||
"{0}-{1}.{ext}",
|
||||
icon_info.name, icon_info.current_t
|
||||
));
|
||||
if file_path.exists() {
|
||||
if icon_info.previous_t.trim() != "" {
|
||||
fs::remove_file(
|
||||
icon_dir.join(format!("{0}-{1}.png", icon_info.name, icon_info.previous_t)),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
fs::remove_file(
|
||||
icon_dir.join(format!("{0}-{1}.ico", icon_info.name, icon_info.previous_t)),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
match fs::copy(file_path, &dest_path) {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::*, feat, module::mihomo::MihomoManager, wrap_err};
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
/// 修改Clash模式
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
feat::change_clash_mode(payload);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 切换Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
log::info!(target: "app", "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global()
|
||||
.change_core(Some(clash_core.clone()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core changed to {clash_core}");
|
||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = err.to_string();
|
||||
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 重启核心
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
}
|
||||
|
||||
/// 获取代理延迟
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
MihomoManager::global()
|
||||
.test_proxy_delay(&name, url, timeout)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 测试URL延迟
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
/// 保存DNS配置到单独文件
|
||||
#[tauri::command]
|
||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
use crate::utils::dirs;
|
||||
use serde_yaml;
|
||||
use std::fs;
|
||||
|
||||
// 获取DNS配置文件路径
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
// 保存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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用或撤销DNS配置
|
||||
#[tauri::command]
|
||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, CoreManager},
|
||||
utils::dirs,
|
||||
};
|
||||
use tauri::async_runtime;
|
||||
|
||||
// 使用spawn来处理异步操作
|
||||
async_runtime::spawn(async move {
|
||||
if apply {
|
||||
// 读取DNS配置文件
|
||||
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);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::warn!(target: "app", "DNS config file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 解析DNS配置并创建patch
|
||||
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||
Ok(config) => {
|
||||
let mut patch = serde_yaml::Mapping::new();
|
||||
patch.insert("dns".into(), config.into());
|
||||
patch
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(target: "app", "Applying DNS config from file");
|
||||
|
||||
// 重新生成配置,确保DNS配置被正确应用
|
||||
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
||||
Config::runtime()
|
||||
.latest()
|
||||
.patch_config(patch_config.clone());
|
||||
|
||||
// 首先重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
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);
|
||||
} else {
|
||||
log::info!(target: "app", "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
} else {
|
||||
// 当关闭DNS设置时,不需要对配置进行任何修改
|
||||
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
||||
log::info!(target: "app", "DNS settings disabled, regenerating config");
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用新配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查DNS配置文件是否存在
|
||||
#[tauri::command]
|
||||
pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
use crate::utils::dirs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
Ok(dns_path.exists())
|
||||
}
|
||||
|
||||
/// 获取DNS配置文件内容
|
||||
#[tauri::command]
|
||||
pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
use crate::utils::dirs;
|
||||
use std::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
if !dns_path.exists() {
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
||||
Ok(content)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use super::CmdResult;
|
||||
use crate::{core::CoreManager, wrap_err};
|
||||
|
||||
/// 修复系统服务
|
||||
#[tauri::command]
|
||||
pub async fn repair_service() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().repair_service().await)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::module::lightweight;
|
||||
|
||||
use super::CmdResult;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||
lightweight::entry_lightweight_mode();
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
use anyhow::Result;
|
||||
|
||||
// Common result type used by command functions
|
||||
pub type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
// Command modules
|
||||
pub mod app;
|
||||
pub mod clash;
|
||||
pub mod core;
|
||||
pub mod media_unlock_checker;
|
||||
pub mod network;
|
||||
pub mod profile;
|
||||
pub mod proxy;
|
||||
pub mod runtime;
|
||||
pub mod save_profile;
|
||||
pub mod system;
|
||||
pub mod uwp;
|
||||
pub mod validate;
|
||||
pub mod verge;
|
||||
pub mod webdav;
|
||||
pub mod lighteweight;
|
||||
|
||||
// Re-export all command functions for backwards compatibility
|
||||
pub use app::*;
|
||||
pub use clash::*;
|
||||
pub use core::*;
|
||||
pub use media_unlock_checker::*;
|
||||
pub use network::*;
|
||||
pub use profile::*;
|
||||
pub use proxy::*;
|
||||
pub use runtime::*;
|
||||
pub use save_profile::*;
|
||||
pub use system::*;
|
||||
pub use uwp::*;
|
||||
pub use validate::*;
|
||||
pub use verge::*;
|
||||
pub use webdav::*;
|
||||
pub use lighteweight::*;
|
||||
@@ -0,0 +1,63 @@
|
||||
use super::CmdResult;
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取网络接口列表
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
use sysinfo::Networks;
|
||||
let mut result = Vec::new();
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
for (interface_name, _) in &networks {
|
||||
result.push(interface_name.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 获取网络接口详细信息
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
||||
|
||||
let names = get_network_interfaces();
|
||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
result.push(interface);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
feat, log_err, ret_err,
|
||||
utils::{dirs, help},
|
||||
wrap_err,
|
||||
};
|
||||
|
||||
/// 获取配置文件列表
|
||||
#[tauri::command]
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok((true, _)) => {
|
||||
println!("[enhance_profiles] 配置更新成功");
|
||||
log_err!(tray::Tray::global().update_tooltip());
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[enhance_profiles] 更新过程发生错误: {}", e);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", e.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 导入配置文件
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// 重新排序配置文件
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
/// 创建配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// 更新配置文件
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
wrap_err!(feat::update_profile(index, option).await)
|
||||
}
|
||||
|
||||
/// 删除配置文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
println!("[cmd配置patch] 开始修改配置文件");
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
|
||||
|
||||
// 更新profiles配置
|
||||
println!("[cmd配置patch] 正在更新配置草稿");
|
||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||
|
||||
// 更新配置并进行验证
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置patch] 配置更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
let _ = tray::Tray::global().update_tooltip();
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
Ok(true)
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
|
||||
Config::profiles().discard();
|
||||
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
println!("[cmd配置patch] 成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置patch] 更新过程发生错误: {}", e);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(
|
||||
_app_handle: tauri::AppHandle,
|
||||
profile_index: String,
|
||||
) -> CmdResult<bool> {
|
||||
let profiles = IProfiles {
|
||||
current: Some(profile_index),
|
||||
items: None,
|
||||
};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 查看配置文件
|
||||
#[tauri::command]
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(app_handle, path))
|
||||
}
|
||||
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use super::CmdResult;
|
||||
use crate::{core::CoreManager, module::mihomo::MihomoManager};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
let mannager = MihomoManager::global();
|
||||
let proxies = mannager
|
||||
.refresh_proxies()
|
||||
.await
|
||||
.map(|_| mannager.get_proxies())
|
||||
.or_else(|_| Ok(mannager.get_proxies()));
|
||||
proxies
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
let mannager = MihomoManager::global();
|
||||
let providers = mannager
|
||||
.refresh_providers_proxies()
|
||||
.await
|
||||
.map(|_| mannager.get_providers_proxies())
|
||||
.or_else(|_| Ok(mannager.get_providers_proxies()));
|
||||
providers
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, wrap_err};
|
||||
use anyhow::Context;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::*, utils::dirs, wrap_err};
|
||||
use std::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let content = wrap_err!(item.read_file())?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||
(profiles_dir.join(path), content, is_merge)
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
println!(
|
||||
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||
file_path_str, is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, Some(true))
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] merge文件语法验证通过");
|
||||
// 成功后尝试更新整体配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非merge文件使用完整验证流程
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, None)
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
|
||||
// 智能判断错误类型
|
||||
let is_script_error = file_path_str.ends_with(".js")
|
||||
|| error_msg.contains("Script syntax error")
|
||||
|| error_msg.contains("Script must contain a main function")
|
||||
|| error_msg.contains("Failed to read script file");
|
||||
|
||||
if error_msg.contains("YAML syntax error")
|
||||
|| error_msg.contains("Failed to read file:")
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{self, handle, service, CoreManager},
|
||||
module::sysinfo::PlatformSpecification,
|
||||
wrap_err,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
sync::atomic::{AtomicI64, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
// 获取当前系统时间,转换为毫秒级时间戳
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
AtomicI64::new(now)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new();
|
||||
let info = format!("{:?}", sysinfo);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cliboard = app_handle.clipboard();
|
||||
if cliboard.write_text(info).is_err() {
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new();
|
||||
let info = format!("{:?}", sysinfo);
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
match CoreManager::global().get_running_mode().await {
|
||||
core::RunningMode::Service => Ok("service".to_string()),
|
||||
core::RunningMode::Sidecar => Ok("sidecar".to_string()),
|
||||
core::RunningMode::NotRunning => Ok("not_running".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 安装/重装系统服务
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
wrap_err!(service::reinstall_service().await)
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
Ok(now - start_time)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use super::CmdResult;
|
||||
|
||||
/// Platform-specific implementation for UWP functionality
|
||||
#[cfg(windows)]
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
use crate::{core::win_uwp, wrap_err};
|
||||
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation for non-Windows platforms
|
||||
#[cfg(not(windows))]
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Command exposed to Tauri
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
platform::invoke_uwp_tool().await
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::*;
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
#[tauri::command]
|
||||
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||
handle::Handle::notice_message(&status, &msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理脚本验证相关的所有消息通知
|
||||
/// 统一通知接口,保持消息类型一致性
|
||||
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
|
||||
// 根据错误消息内容判断错误类型
|
||||
let status = if error_msg.starts_with("File not found:") {
|
||||
"config_validate::file_not_found"
|
||||
} else if error_msg.starts_with("Failed to read script file:") {
|
||||
"config_validate::script_error"
|
||||
} else if error_msg.starts_with("Script syntax error:") {
|
||||
"config_validate::script_syntax_error"
|
||||
} else if error_msg == "Script must contain a main function" {
|
||||
"config_validate::script_missing_main"
|
||||
} else {
|
||||
// 如果是其他类型错误,作为一般脚本错误处理
|
||||
"config_validate::script_error"
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理YAML验证相关的所有消息通知
|
||||
/// 统一通知接口,保持消息类型一致性
|
||||
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||
|
||||
// 检查是否为merge文件
|
||||
let is_merge_file = file_type.contains("合并");
|
||||
|
||||
// 根据错误消息内容判断错误类型
|
||||
let status = if error_msg.starts_with("File not found:") {
|
||||
"config_validate::file_not_found"
|
||||
} else if error_msg.starts_with("Failed to read file:") {
|
||||
"config_validate::yaml_read_error"
|
||||
} else if error_msg.starts_with("YAML syntax error:") {
|
||||
if is_merge_file {
|
||||
"config_validate::merge_syntax_error"
|
||||
} else {
|
||||
"config_validate::yaml_syntax_error"
|
||||
}
|
||||
} else if error_msg.contains("mapping values are not allowed") {
|
||||
if is_merge_file {
|
||||
"config_validate::merge_mapping_error"
|
||||
} else {
|
||||
"config_validate::yaml_mapping_error"
|
||||
}
|
||||
} else if error_msg.contains("did not find expected key") {
|
||||
if is_merge_file {
|
||||
"config_validate::merge_key_error"
|
||||
} else {
|
||||
"config_validate::yaml_key_error"
|
||||
}
|
||||
} else {
|
||||
// 如果是其他类型错误,根据文件类型作为一般错误处理
|
||||
if is_merge_file {
|
||||
"config_validate::merge_error"
|
||||
} else {
|
||||
"config_validate::yaml_error"
|
||||
}
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, feat, wrap_err};
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload, false).await)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core, feat, wrap_err};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
|
||||
/// 保存 WebDAV 配置
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 创建 WebDAV 备份并上传
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
}
|
||||
|
||||
/// 列出 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
}
|
||||
|
||||
/// 删除 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
/// 从 WebDAV 恢复备份文件
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
}
|
||||
@@ -1,608 +0,0 @@
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use crate::{log_err, ret_err, wrap_err};
|
||||
use anyhow::{Context, Result};
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use tauri::Manager;
|
||||
use std::fs;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok((true, _)) => {
|
||||
println!("[enhance_profiles] 配置更新成功");
|
||||
log_err!(tray::Tray::global().update_tooltip());
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[enhance_profiles] 更新过程发生错误: {}", e);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &e.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
wrap_err!(feat::update_profile(index, option).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(
|
||||
profiles: IProfiles
|
||||
) -> CmdResult<bool> {
|
||||
println!("[cmd配置patch] 开始修改配置文件");
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
|
||||
|
||||
// 更新profiles配置
|
||||
println!("[cmd配置patch] 正在更新配置草稿");
|
||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||
|
||||
// 更新配置并进行验证
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置patch] 配置更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
let _ = tray::Tray::global().update_tooltip();
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
Ok(true)
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
|
||||
Config::profiles().discard();
|
||||
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
println!("[cmd配置patch] 成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置patch] 更新过程发生错误: {}", e);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", &e.to_string());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(
|
||||
_app_handle: tauri::AppHandle,
|
||||
profile_index: String
|
||||
) -> CmdResult<bool> {
|
||||
let profiles = IProfiles{current: Some(profile_index), items: None};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(app_handle, path))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
let content = wrap_err!(item.read_file())?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||
(profiles_dir.join(path), content)
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy();
|
||||
println!("[cmd配置save] 开始验证配置文件: {}", file_path_str);
|
||||
|
||||
// 验证配置文件
|
||||
match CoreManager::global().validate_config_file(&file_path_str).await {
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
|
||||
// 智能判断是否为脚本错误
|
||||
let is_script_error = file_path_str.ends_with(".js") ||
|
||||
error_msg.contains("Script syntax error") ||
|
||||
error_msg.contains("Script must contain a main function") ||
|
||||
error_msg.contains("Failed to read script file");
|
||||
|
||||
if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
let result = (false, error_msg.clone());
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
Ok(feat::change_clash_mode(payload))
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
log::info!(target: "app", "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global().change_core(Some(clash_core.clone())).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core changed to {clash_core}");
|
||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = err.to_string();
|
||||
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// restart the sidecar
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod uwp {
|
||||
use super::*;
|
||||
use crate::core::win_uwp;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<clash_api::DelayRes> {
|
||||
match clash_api::get_proxy_delay(name, url, timeout).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||
let icon_path = icon_cache_dir.join(name);
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
}
|
||||
if !icon_path.exists() {
|
||||
let response = wrap_err!(reqwest::get(url).await)?;
|
||||
|
||||
let mut file = wrap_err!(std::fs::File::create(&icon_path))?;
|
||||
|
||||
let content = wrap_err!(response.bytes().await)?;
|
||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||
}
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
|
||||
let file_path = std::path::Path::new(&path);
|
||||
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||
if !icon_dir.exists() {
|
||||
let _ = std::fs::create_dir_all(&icon_dir);
|
||||
}
|
||||
let ext = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().to_string(),
|
||||
None => "ico".to_string(),
|
||||
};
|
||||
|
||||
let png_dest_path = icon_dir.join(format!("{name}.png"));
|
||||
let ico_dest_path = icon_dir.join(format!("{name}.ico"));
|
||||
let dest_path = icon_dir.join(format!("{name}.{ext}"));
|
||||
if file_path.exists() {
|
||||
std::fs::remove_file(png_dest_path).unwrap_or_default();
|
||||
std::fs::remove_file(ico_dest_path).unwrap_or_default();
|
||||
match std::fs::copy(file_path, &dest_path) {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
use sysinfo::Networks;
|
||||
let mut result = Vec::new();
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
for (interface_name, _) in &networks {
|
||||
result.push(interface_name.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
use network_interface::NetworkInterface;
|
||||
use network_interface::NetworkInterfaceConfig;
|
||||
|
||||
let names = get_network_interfaces();
|
||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
result.push(interface);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if !window.is_devtools_open() {
|
||||
window.open_devtools();
|
||||
} else {
|
||||
window.close_devtools();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn exit_app() {
|
||||
feat::quit(Some(0));
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod uwp {
|
||||
use super::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||
handle::Handle::notice_message(&status, &msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理脚本验证相关的所有消息通知
|
||||
/// 统一通知接口,保持消息类型一致性
|
||||
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
|
||||
// 根据错误消息内容判断错误类型
|
||||
let status = if error_msg.starts_with("File not found:") {
|
||||
"config_validate::file_not_found"
|
||||
} else if error_msg.starts_with("Failed to read script file:") {
|
||||
"config_validate::script_error"
|
||||
} else if error_msg.starts_with("Script syntax error:") {
|
||||
"config_validate::script_syntax_error"
|
||||
} else if error_msg == "Script must contain a main function" {
|
||||
"config_validate::script_missing_main"
|
||||
} else {
|
||||
// 如果是其他类型错误,作为一般脚本错误处理
|
||||
"config_validate::script_error"
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreManager::global().validate_config_file(&file_path).await {
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
},
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,12 @@ impl IClashTemp {
|
||||
let mut map = Mapping::new();
|
||||
let mut tun = Mapping::new();
|
||||
tun.insert("enable".into(), false.into());
|
||||
tun.insert("stack".into(), "mixed".into());
|
||||
tun.insert("stack".into(), "gvisor".into());
|
||||
tun.insert("auto-route".into(), true.into());
|
||||
tun.insert("strict-route".into(), false.into());
|
||||
tun.insert("auto-detect-interface".into(), true.into());
|
||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
map.insert("redir-port".into(), 7895.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
config::PrfItem,
|
||||
core::{handle, CoreManager},
|
||||
enhance,
|
||||
utils::{dirs, help},
|
||||
core::{handle, CoreManager},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -73,14 +73,17 @@ impl Config {
|
||||
// 生成运行时配置文件并验证
|
||||
let config_result = Self::generate_file(ConfigType::Run);
|
||||
|
||||
let validation_result = if let Ok(_) = config_result {
|
||||
let validation_result = if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
println!("[首次启动] 开始验证配置");
|
||||
|
||||
|
||||
match CoreManager::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
if !is_valid {
|
||||
println!("[首次启动] 配置验证失败,使用默认最小配置启动: {}", error_msg);
|
||||
println!(
|
||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
@@ -101,10 +104,7 @@ impl Config {
|
||||
} else {
|
||||
println!("[首次启动] 生成配置文件失败,使用默认配置");
|
||||
CoreManager::global()
|
||||
.use_default_config(
|
||||
"config_validate::error",
|
||||
"",
|
||||
)
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
// Generate random nonce
|
||||
let mut nonce = vec![0u8; NONCE_LENGTH];
|
||||
getrandom::getrandom(&mut nonce)?;
|
||||
getrandom::fill(&mut nonce)?;
|
||||
|
||||
// Encrypt data
|
||||
let ciphertext = cipher
|
||||
|
||||
@@ -8,14 +8,9 @@ mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::clash::*;
|
||||
pub use self::config::*;
|
||||
pub use self::draft::*;
|
||||
pub use self::encrypt::*;
|
||||
pub use self::prfitem::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::runtime::*;
|
||||
pub use self::verge::*;
|
||||
pub use self::{
|
||||
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
||||
};
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
|
||||
@@ -234,10 +234,10 @@ impl PrfItem {
|
||||
option: Option<PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let opt_ref = option.as_ref();
|
||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let accept_invalid_certs =
|
||||
opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||
|
||||
@@ -472,15 +472,17 @@ impl IProfiles {
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||
match self.items.as_ref() {
|
||||
Some(items) => Some(items.iter().filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||
Some((uid, name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect()),
|
||||
None => None,
|
||||
}
|
||||
self.items.as_ref().map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||
Some((uid, name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::DEFAULT_PAC;
|
||||
use crate::config::{deserialize_encrypted, serialize_encrypted};
|
||||
use crate::utils::i18n;
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{
|
||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
||||
utils::{dirs, help, i18n},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -70,6 +70,9 @@ pub struct IVerge {
|
||||
/// enable proxy guard
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
|
||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
|
||||
/// always use default bypass
|
||||
pub use_default_bypass: Option<bool>,
|
||||
|
||||
@@ -98,10 +101,14 @@ pub struct IVerge {
|
||||
/// hotkey map
|
||||
/// format: {func},{key}
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
|
||||
/// enable global hotkey
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
|
||||
/// 首页卡片设置
|
||||
/// 控制首页各个卡片的显示和隐藏
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
|
||||
/// 切换代理时自动关闭连接
|
||||
pub auto_close_connection: Option<bool>,
|
||||
|
||||
@@ -182,8 +189,16 @@ pub struct IVerge {
|
||||
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
/// 轻量模式 - 只保留内核运行
|
||||
pub enable_lite_mode: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式的延迟(分钟)
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
|
||||
/// 服务状态跟踪
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -245,7 +260,7 @@ impl IVerge {
|
||||
env_type: Some("bash".into()),
|
||||
#[cfg(target_os = "windows")]
|
||||
env_type: Some("powershell".into()),
|
||||
start_page: Some("/".into()),
|
||||
start_page: Some("/home".into()),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_group_icon: Some(true),
|
||||
@@ -285,8 +300,13 @@ impl IVerge {
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(true),
|
||||
enable_tray_icon: Some(true),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_lite_mode: Some(false),
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
enable_dns_settings: Some(true),
|
||||
home_cards: None,
|
||||
service_state: None,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -368,7 +388,12 @@ impl IVerge {
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
patch!(enable_lite_mode);
|
||||
patch!(enable_tray_icon);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(service_state);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
@@ -457,7 +482,12 @@ pub struct IVergeResponse {
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
pub enable_lite_mode: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
@@ -520,7 +550,12 @@ impl From<IVerge> for IVergeResponse {
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
enable_lite_mode: verge.enable_lite_mode,
|
||||
enable_tray_icon: verge.enable_tray_icon,
|
||||
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,
|
||||
home_cards: verge.home_cards,
|
||||
service_state: verge.service_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use anyhow::Error;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||
use std::collections::HashMap;
|
||||
use std::env::{consts::OS, temp_dir};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{consts::OS, temp_dir},
|
||||
fs,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Rate {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
/// PUT /configs
|
||||
/// path 是绝对路径
|
||||
pub async fn put_configs(path: &str) -> Result<()> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/configs?force=true");
|
||||
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", path);
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.put(&url).headers(headers).json(&data);
|
||||
let response = builder.send().await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
204 => Ok(()),
|
||||
status => {
|
||||
let body = response.text().await?;
|
||||
// print!("failed to put configs with status \"{}\"\n{}\n{}", status, url, body);
|
||||
bail!("failed to put configs with status \"{status}\"\n{url}\n{body}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PATCH /configs
|
||||
pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/configs");
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.patch(&url).headers(headers.clone()).json(config);
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct DelayRes {
|
||||
delay: u64,
|
||||
}
|
||||
|
||||
/// GET /proxies/{name}/delay
|
||||
/// 获取代理延迟
|
||||
pub async fn get_proxy_delay(
|
||||
name: String,
|
||||
test_url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> Result<DelayRes> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/proxies/{name}/delay");
|
||||
|
||||
let default_url = "http://cp.cloudflare.com/generate_204";
|
||||
let test_url = test_url
|
||||
.map(|s| if s.is_empty() { default_url.into() } else { s })
|
||||
.unwrap_or(default_url.into());
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client
|
||||
.get(&url)
|
||||
.headers(headers)
|
||||
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
|
||||
let response = builder.send().await?;
|
||||
|
||||
Ok(response.json::<DelayRes>().await?)
|
||||
}
|
||||
|
||||
/// 根据clash info获取clash服务地址和请求头
|
||||
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
|
||||
let server = format!("http://{}", client.server);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse()?);
|
||||
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse()?;
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Ok((server, headers))
|
||||
}
|
||||
|
||||
/// 缩短clash的日志
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_log(log: String) -> String {
|
||||
if log.starts_with("time=") && log.len() > 33 {
|
||||
return (log[33..]).to_owned();
|
||||
}
|
||||
if log.len() > 9 {
|
||||
return (log[9..]).to_owned();
|
||||
}
|
||||
log
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_check_output(log: String) -> String {
|
||||
let t = log.find("time=");
|
||||
let m = log.find("msg=");
|
||||
let mr = log.rfind('"');
|
||||
|
||||
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
|
||||
let e = match log.find("level=error msg=") {
|
||||
Some(e) => e + 17,
|
||||
None => m + 5,
|
||||
};
|
||||
|
||||
if mr > m {
|
||||
return (log[e..mr]).to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
let l = log.find("error=");
|
||||
let r = log.find("path=").or(Some(log.len()));
|
||||
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
return (log[(l + 6)..(r - 1)]).to_owned();
|
||||
}
|
||||
|
||||
log
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_traffic_ws_url() -> Result<String> {
|
||||
let (url, _) = clash_client_info()?;
|
||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
||||
Ok(ws_url)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_check_output() {
|
||||
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
|
||||
//let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
|
||||
let str3 = r#"
|
||||
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
|
||||
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
|
||||
configuration file xxx\n
|
||||
"#;
|
||||
|
||||
let res1 = parse_check_output(str1.into());
|
||||
// let res2 = parse_check_output(str2.into());
|
||||
let res3 = parse_check_output(str3.into());
|
||||
|
||||
assert_eq!(res1, res3);
|
||||
}
|
||||
+641
-78
@@ -1,22 +1,37 @@
|
||||
use crate::config::*;
|
||||
use crate::core::{clash_api, handle, service};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::core::tray::Tray;
|
||||
use crate::log_err;
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{
|
||||
config::*,
|
||||
core::{handle, service},
|
||||
log_err,
|
||||
module::mihomo::MihomoManager,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use fs2::FileExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tokio::{sync::Mutex, time::sleep};
|
||||
|
||||
use super::service::is_service_running;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
running: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
/// 内核运行模式
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub enum RunningMode {
|
||||
/// 服务模式运行
|
||||
Service,
|
||||
/// Sidecar模式运行
|
||||
Sidecar,
|
||||
/// 未运行
|
||||
NotRunning,
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn global() -> &'static CoreManager {
|
||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||
@@ -38,27 +53,84 @@ impl CoreManager {
|
||||
let mut running = self.running.lock().await;
|
||||
|
||||
if !*running {
|
||||
println!("[停止内核] 内核未运行");
|
||||
log::debug!("core is not running");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("[停止内核] 开始停止内核");
|
||||
|
||||
// 关闭tun模式
|
||||
let mut disable = Mapping::new();
|
||||
let mut tun = Mapping::new();
|
||||
tun.insert("enable".into(), false.into());
|
||||
disable.insert("tun".into(), tun.into());
|
||||
// Create a JSON object to disable TUN mode
|
||||
let disable = serde_json::json!({
|
||||
"tun": {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
println!("[停止内核] 禁用TUN模式");
|
||||
log::debug!(target: "app", "disable tun mode");
|
||||
log_err!(clash_api::patch_configs(&disable).await);
|
||||
log_err!(MihomoManager::global().patch_configs(disable).await);
|
||||
|
||||
// 服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
println!("[停止内核] 尝试通过服务停止内核");
|
||||
log::info!(target: "app", "stop the core by service");
|
||||
service::stop_core_by_service().await?;
|
||||
match service::stop_core_by_service().await {
|
||||
Ok(_) => {
|
||||
println!("[停止内核] 服务模式下内核停止成功");
|
||||
log::info!(target: "app", "core stopped successfully by service");
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[停止内核] 服务模式下停止内核失败: {}", err);
|
||||
println!("[停止内核] 尝试停止可能的sidecar进程");
|
||||
log::warn!(target: "app", "failed to stop core by service: {}", err);
|
||||
// 服务停止失败,尝试停止可能的sidecar进程
|
||||
self.stop_sidecar_process();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有使用服务,尝试停止sidecar进程
|
||||
println!("[停止内核] 服务不可用,尝试停止sidecar进程");
|
||||
self.stop_sidecar_process();
|
||||
}
|
||||
|
||||
// 释放文件锁
|
||||
println!("[停止内核] 尝试释放文件锁");
|
||||
if let Some(_) = handle::Handle::global().release_core_lock() {
|
||||
println!("[停止内核] 文件锁释放成功");
|
||||
log::info!(target: "app", "released core lock file");
|
||||
} else {
|
||||
println!("[停止内核] 没有文件锁需要释放");
|
||||
}
|
||||
|
||||
*running = false;
|
||||
println!("[停止内核] 内核停止完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止通过sidecar启动的进程
|
||||
fn stop_sidecar_process(&self) {
|
||||
if let Some(process) = handle::Handle::global().take_core_process() {
|
||||
println!("[停止sidecar] 发现sidecar进程,准备停止");
|
||||
log::info!(target: "app", "stopping core process in sidecar mode");
|
||||
|
||||
// 尝试获取进程ID
|
||||
let pid = process.pid();
|
||||
println!("[停止sidecar] 进程PID: {}", pid);
|
||||
|
||||
// 尝试终止进程
|
||||
if let Err(e) = process.kill() {
|
||||
println!("[停止sidecar] 终止sidecar进程失败: {}", e);
|
||||
log::warn!(target: "app", "failed to kill core process: {}", e);
|
||||
} else {
|
||||
println!("[停止sidecar] sidecar进程已成功终止");
|
||||
log::info!(target: "app", "core process stopped successfully");
|
||||
}
|
||||
} else {
|
||||
println!("[停止sidecar] 没有找到sidecar进程");
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动核心
|
||||
pub async fn start_core(&self) -> Result<()> {
|
||||
let mut running = self.running.lock().await;
|
||||
@@ -69,11 +141,28 @@ impl CoreManager {
|
||||
|
||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||
|
||||
// 服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
// 先检查服务状态
|
||||
let service_available = service::check_service().await.is_ok();
|
||||
|
||||
if service_available {
|
||||
log::info!(target: "app", "try to run core in service mode");
|
||||
service::run_core_by_service(&config_path).await?;
|
||||
match service::run_core_by_service(&config_path).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core started successfully in service mode");
|
||||
}
|
||||
Err(err) => {
|
||||
// 服务启动失败,直接尝试sidecar模式,不再尝试重装服务
|
||||
log::warn!(target: "app", "failed to start core in service mode: {}", err);
|
||||
log::info!(target: "app", "trying to run core in sidecar mode");
|
||||
self.run_core_by_sidecar(&config_path).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 服务不可用,直接使用sidecar模式
|
||||
log::info!(target: "app", "service not available, running core in sidecar mode");
|
||||
self.run_core_by_sidecar(&config_path).await?;
|
||||
}
|
||||
|
||||
// 流量订阅
|
||||
#[cfg(target_os = "macos")]
|
||||
log_err!(Tray::global().subscribe_traffic().await);
|
||||
@@ -83,11 +172,154 @@ impl CoreManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 通过sidecar启动内核
|
||||
async fn run_core_by_sidecar(&self, config_path: &PathBuf) -> Result<()> {
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
|
||||
log::info!(target: "app", "starting core {} in sidecar mode", clash_core);
|
||||
println!("[sidecar启动] 开始以sidecar模式启动内核: {}", clash_core);
|
||||
|
||||
// 检查系统中是否存在同名进程
|
||||
if let Ok(pids) = self.check_existing_processes(&clash_core).await {
|
||||
if !pids.is_empty() {
|
||||
println!("[sidecar启动] 警告:系统中已存在同名进程");
|
||||
// 尝试检查端口占用
|
||||
if let Ok(config_content) = std::fs::read_to_string(config_path) {
|
||||
if let Ok(config) = serde_yaml::from_str::<serde_yaml::Value>(&config_content) {
|
||||
// 获取配置中定义的端口
|
||||
let mixed_port = config
|
||||
.get("mixed-port")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(7890);
|
||||
let http_port = config.get("port").and_then(|v| v.as_u64()).unwrap_or(7890);
|
||||
|
||||
println!(
|
||||
"[sidecar启动] 检查端口占用: HTTP端口={}, 混合端口={}",
|
||||
http_port, mixed_port
|
||||
);
|
||||
|
||||
// 检查端口是否被占用
|
||||
if self.is_port_in_use(mixed_port as u16).await
|
||||
|| self.is_port_in_use(http_port as u16).await
|
||||
{
|
||||
println!("[sidecar启动] 端口已被占用,尝试终止已存在的进程");
|
||||
|
||||
// 尝试终止已存在的进程
|
||||
for pid in pids {
|
||||
println!("[sidecar启动] 尝试终止进程 PID: {}", pid);
|
||||
self.terminate_process(pid).await;
|
||||
}
|
||||
|
||||
// 等待短暂时间让资源释放
|
||||
println!("[sidecar启动] 等待500ms让资源释放");
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("[sidecar启动] 无法检查系统进程,继续尝试启动");
|
||||
}
|
||||
|
||||
// 创建锁文件路径
|
||||
let lock_file = dirs::app_home_dir()?.join(format!("{}.lock", clash_core));
|
||||
println!("[sidecar启动] 锁文件路径: {:?}", lock_file);
|
||||
|
||||
// 尝试获取文件锁
|
||||
println!("[sidecar启动] 尝试获取文件锁");
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_file)?;
|
||||
|
||||
match file.try_lock_exclusive() {
|
||||
Ok(_) => {
|
||||
// 成功获取锁,说明没有其他实例运行
|
||||
println!("[sidecar启动] 成功获取文件锁,没有检测到其他运行的实例");
|
||||
log::info!(target: "app", "acquired lock for core process");
|
||||
|
||||
// 保存锁对象到全局,防止被Drop
|
||||
handle::Handle::global().set_core_lock(file);
|
||||
}
|
||||
Err(err) => {
|
||||
// 无法获取锁,说明已有实例运行
|
||||
println!("[sidecar启动] 无法获取文件锁,检测到其他实例可能正在运行");
|
||||
println!("[sidecar启动] 错误信息: {:?}", err);
|
||||
log::warn!(target: "app", "another core process appears to be running");
|
||||
|
||||
// 尝试强制获取锁(可能会导致其他进程崩溃)
|
||||
println!("[sidecar启动] 尝试强制删除并重新创建锁文件");
|
||||
std::fs::remove_file(&lock_file)?;
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_file)?;
|
||||
|
||||
println!("[sidecar启动] 尝试强制获取锁");
|
||||
match file.lock_exclusive() {
|
||||
Ok(_) => println!("[sidecar启动] 成功强制获取锁"),
|
||||
Err(e) => println!("[sidecar启动] 强制获取锁失败: {:?}", e),
|
||||
}
|
||||
file.lock_exclusive()?;
|
||||
|
||||
// 保存新锁
|
||||
handle::Handle::global().set_core_lock(file);
|
||||
|
||||
// 等待可能的其他进程退出
|
||||
println!("[sidecar启动] 等待500ms,让可能的其他进程退出");
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||
|
||||
// 获取配置目录
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
let config_path_str = dirs::path_to_str(config_path)?;
|
||||
|
||||
// 启动核心进程并转入后台运行
|
||||
println!("[sidecar启动] 开始启动核心进程");
|
||||
let (_, child) = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)?
|
||||
.args(["-d", dirs::path_to_str(&config_dir)?, "-f", config_path_str])
|
||||
.spawn()?;
|
||||
|
||||
// 保存进程ID以便后续管理
|
||||
println!("[sidecar启动] 核心进程启动成功,PID: {:?}", child.pid());
|
||||
handle::Handle::global().set_core_process(child);
|
||||
|
||||
// 等待短暂时间确保启动成功
|
||||
sleep(Duration::from_millis(300)).await;
|
||||
|
||||
println!("[sidecar启动] 内核启动完成");
|
||||
log::info!(target: "app", "core started in sidecar mode");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重启内核
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
// 重新启动app
|
||||
log::info!(target: "app", "restarting core");
|
||||
self.stop_core().await?;
|
||||
self.start_core().await?;
|
||||
log::info!(target: "app", "core restarted successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 强制重新安装服务(供UI调用,用户主动修复服务)
|
||||
pub async fn repair_service(&self) -> Result<()> {
|
||||
log::info!(target: "app", "user requested service repair");
|
||||
|
||||
// 调用强制重装服务
|
||||
service::force_reinstall_service().await?;
|
||||
|
||||
// 重启核心
|
||||
self.restart_core().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -118,10 +350,10 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
log::info!(target: "app", "change core to `{clash_core}`");
|
||||
|
||||
|
||||
// 1. 先更新内核配置(但不应用)
|
||||
Config::verge().draft().clash_core = Some(clash_core);
|
||||
|
||||
|
||||
// 2. 使用新内核验证配置
|
||||
println!("[切换内核] 使用新内核验证配置");
|
||||
match self.validate_config().await {
|
||||
@@ -130,7 +362,7 @@ impl CoreManager {
|
||||
// 3. 验证通过后,应用内核配置并重启
|
||||
Config::verge().apply();
|
||||
log_err!(Config::verge().latest().save_file());
|
||||
|
||||
|
||||
match self.restart_core().await {
|
||||
Ok(_) => {
|
||||
println!("[切换内核] 内核切换成功");
|
||||
@@ -139,19 +371,22 @@ impl CoreManager {
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[切换内核] 内核切换失败: {}", err);
|
||||
Config::verge().discard();
|
||||
Config::runtime().discard();
|
||||
Err(err)
|
||||
// 即使使用服务失败,我们也尝试使用sidecar模式启动
|
||||
log::info!(target: "app", "trying sidecar mode after service failure");
|
||||
self.start_core().await?;
|
||||
Config::runtime().apply();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[切换内核] 配置验证失败: {}", error_msg);
|
||||
// 使用默认配置并继续切换内核
|
||||
self.use_default_config("config_validate::core_change", &error_msg).await?;
|
||||
self.use_default_config("config_validate::core_change", &error_msg)
|
||||
.await?;
|
||||
Config::verge().apply();
|
||||
log_err!(Config::verge().latest().save_file());
|
||||
|
||||
|
||||
match self.restart_core().await {
|
||||
Ok(_) => {
|
||||
println!("[切换内核] 内核切换成功(使用默认配置)");
|
||||
@@ -159,8 +394,10 @@ impl CoreManager {
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[切换内核] 内核切换失败: {}", err);
|
||||
Config::verge().discard();
|
||||
Err(err)
|
||||
// 即使使用服务失败,我们也尝试使用sidecar模式启动
|
||||
log::info!(target: "app", "trying sidecar mode after service failure with default config");
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,12 +411,18 @@ impl CoreManager {
|
||||
|
||||
/// 内部验证配置文件的实现
|
||||
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
println!("[core配置验证] 应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
println!("[core配置验证] 开始验证配置文件: {}", config_path);
|
||||
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
println!("[core配置验证] 使用内核: {}", clash_core);
|
||||
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let test_dir = dirs::app_home_dir()?.join("test");
|
||||
let test_dir = dirs::path_to_str(&test_dir)?;
|
||||
@@ -196,14 +439,15 @@ impl CoreManager {
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
|
||||
// 检查进程退出状态和错误输出
|
||||
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
|
||||
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
let has_error =
|
||||
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
|
||||
|
||||
println!("\n[core配置验证] -------- 验证结果 --------");
|
||||
println!("[core配置验证] 进程退出状态: {:?}", output.status);
|
||||
|
||||
|
||||
if !stderr.is_empty() {
|
||||
println!("[core配置验证] stderr输出:\n{}", stderr);
|
||||
}
|
||||
@@ -224,7 +468,7 @@ impl CoreManager {
|
||||
};
|
||||
|
||||
println!("[core配置验证] -------- 验证结束 --------\n");
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||
} else {
|
||||
println!("[core配置验证] 验证成功");
|
||||
println!("[core配置验证] -------- 验证结束 --------\n");
|
||||
@@ -240,14 +484,33 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
/// 验证指定的配置文件
|
||||
pub async fn validate_config_file(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
pub async fn validate_config_file(
|
||||
&self,
|
||||
config_path: &str,
|
||||
is_merge_file: Option<bool>,
|
||||
) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过验证
|
||||
if handle::Handle::global().is_exiting() {
|
||||
println!("[core配置验证] 应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
|
||||
if is_merge_file.unwrap_or(false) {
|
||||
println!(
|
||||
"[core配置验证] 检测到Merge文件,仅进行语法检查: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_file_syntax(config_path).await;
|
||||
}
|
||||
|
||||
// 检查是否为脚本文件
|
||||
let is_script = if config_path.ends_with(".js") {
|
||||
true
|
||||
@@ -261,12 +524,12 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if is_script {
|
||||
log::info!(target: "app", "检测到脚本文件,使用JavaScript验证: {}", config_path);
|
||||
return self.validate_script_file(config_path).await;
|
||||
}
|
||||
|
||||
|
||||
// 对YAML配置文件使用Clash内核验证
|
||||
log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path);
|
||||
self.validate_config_internal(config_path).await
|
||||
@@ -280,33 +543,36 @@ impl CoreManager {
|
||||
} else if path.ends_with(".js") {
|
||||
return Ok(true); // JS文件是脚本文件
|
||||
}
|
||||
|
||||
|
||||
// 2. 读取文件内容
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err);
|
||||
return Err(anyhow::anyhow!("Failed to read file to detect type: {}", err));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to read file to detect type: {}",
|
||||
err
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 3. 检查是否存在明显的YAML特征
|
||||
let has_yaml_features = content.contains(": ") ||
|
||||
content.contains("#") ||
|
||||
content.contains("---") ||
|
||||
content.lines().any(|line| line.trim().starts_with("- "));
|
||||
|
||||
let has_yaml_features = content.contains(": ")
|
||||
|| content.contains("#")
|
||||
|| content.contains("---")
|
||||
|| content.lines().any(|line| line.trim().starts_with("- "));
|
||||
|
||||
// 4. 检查是否存在明显的JS特征
|
||||
let has_js_features = content.contains("function ") ||
|
||||
content.contains("const ") ||
|
||||
content.contains("let ") ||
|
||||
content.contains("var ") ||
|
||||
content.contains("//") ||
|
||||
content.contains("/*") ||
|
||||
content.contains("*/") ||
|
||||
content.contains("export ") ||
|
||||
content.contains("import ");
|
||||
|
||||
let has_js_features = content.contains("function ")
|
||||
|| content.contains("const ")
|
||||
|| content.contains("let ")
|
||||
|| content.contains("var ")
|
||||
|| content.contains("//")
|
||||
|| content.contains("/*")
|
||||
|| content.contains("*/")
|
||||
|| content.contains("export ")
|
||||
|| content.contains("import ");
|
||||
|
||||
// 5. 决策逻辑
|
||||
if has_yaml_features && !has_js_features {
|
||||
// 只有YAML特征,没有JS特征
|
||||
@@ -317,22 +583,21 @@ impl CoreManager {
|
||||
} else if has_yaml_features && has_js_features {
|
||||
// 两种特征都有,需要更精细判断
|
||||
// 优先检查是否有明确的JS结构特征
|
||||
if content.contains("function main") ||
|
||||
content.contains("module.exports") ||
|
||||
content.contains("export default") {
|
||||
if content.contains("function main")
|
||||
|| content.contains("module.exports")
|
||||
|| content.contains("export default")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
|
||||
// 检查冒号后是否有空格(YAML的典型特征)
|
||||
let yaml_pattern_count = content.lines()
|
||||
.filter(|line| line.contains(": "))
|
||||
.count();
|
||||
|
||||
let yaml_pattern_count = content.lines().filter(|line| line.contains(": ")).count();
|
||||
|
||||
if yaml_pattern_count > 2 {
|
||||
return Ok(false); // 多个键值对格式,更可能是YAML
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 默认情况:无法确定时,假设为非脚本文件(更安全)
|
||||
log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path);
|
||||
Ok(false)
|
||||
@@ -345,33 +610,37 @@ impl CoreManager {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_error", &error_msg);
|
||||
log::warn!(target: "app", "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
log::debug!(target: "app", "验证脚本文件: {}", path);
|
||||
|
||||
|
||||
// 使用boa引擎进行基本语法检查
|
||||
use boa_engine::{Context, Source};
|
||||
|
||||
|
||||
let mut context = Context::default();
|
||||
let result = context.eval(Source::from_bytes(&content));
|
||||
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::debug!(target: "app", "脚本语法验证通过: {}", path);
|
||||
|
||||
|
||||
// 检查脚本是否包含main函数
|
||||
if !content.contains("function main") && !content.contains("const main") && !content.contains("let main") {
|
||||
if !content.contains("function main")
|
||||
&& !content.contains("const main")
|
||||
&& !content.contains("let main")
|
||||
{
|
||||
let error_msg = "Script must contain a main function";
|
||||
log::warn!(target: "app", "脚本缺少main函数: {}", path);
|
||||
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
|
||||
return Ok((false, error_msg.to_string()));
|
||||
}
|
||||
|
||||
|
||||
Ok((true, String::new()))
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {}", err);
|
||||
log::warn!(target: "app", "脚本语法错误: {}", err);
|
||||
@@ -383,12 +652,18 @@ impl CoreManager {
|
||||
|
||||
/// 更新proxies等配置
|
||||
pub async fn update_config(&self) -> Result<(bool, String)> {
|
||||
// 检查程序是否正在退出,如果是则跳过完整验证流程
|
||||
if handle::Handle::global().is_exiting() {
|
||||
println!("[core配置更新] 应用正在退出,跳过验证");
|
||||
return Ok((true, String::new()));
|
||||
}
|
||||
|
||||
println!("[core配置更新] 开始更新配置");
|
||||
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
println!("[core配置更新] 生成新的配置内容");
|
||||
Config::generate().await?;
|
||||
|
||||
|
||||
// 2. 生成临时文件并进行验证
|
||||
println!("[core配置更新] 生成临时配置文件用于验证");
|
||||
let temp_config = Config::generate_file(ConfigType::Check)?;
|
||||
@@ -407,7 +682,9 @@ impl CoreManager {
|
||||
// 5. 应用新配置
|
||||
println!("[core配置更新] 应用新配置");
|
||||
for i in 0..3 {
|
||||
match clash_api::put_configs(run_path).await {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
|
||||
match MihomoManager::global().put_configs_force(run_path).await {
|
||||
Ok(_) => {
|
||||
println!("[core配置更新] 配置应用成功");
|
||||
Config::runtime().apply();
|
||||
@@ -440,4 +717,290 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
println!("[core配置语法检查] 开始检查文件: {}", 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);
|
||||
println!("[core配置语法检查] 无法读取文件: {}", error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
};
|
||||
|
||||
// 对YAML文件尝试解析,只检查语法正确性
|
||||
println!("[core配置语法检查] 进行YAML语法检查");
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||
Ok(_) => {
|
||||
println!("[core配置语法检查] YAML语法检查通过");
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {}", err);
|
||||
println!("[core配置语法检查] YAML语法错误: {}", error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
pub async fn get_running_mode(&self) -> RunningMode {
|
||||
let running = self.running.lock().await;
|
||||
if !*running {
|
||||
return RunningMode::NotRunning;
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
match service::check_service().await {
|
||||
Ok(_) => {
|
||||
// 检查服务是否实际运行核心
|
||||
match service::is_service_running().await {
|
||||
Ok(true) => RunningMode::Service,
|
||||
_ => {
|
||||
// 服务存在但可能没有运行,检查是否有sidecar进程
|
||||
if handle::Handle::global().has_core_process() {
|
||||
// 检查是否持有文件锁,确保是由我们启动的进程
|
||||
if handle::Handle::global().has_core_lock() {
|
||||
RunningMode::Sidecar
|
||||
} else {
|
||||
// 有进程但没有文件锁,可能是外部启动的进程
|
||||
log::warn!(target: "app", "core process exists but no lock file");
|
||||
RunningMode::Sidecar // 仍返回Sidecar模式,但记录了警告
|
||||
}
|
||||
} else {
|
||||
RunningMode::NotRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// 服务不可用,检查是否有sidecar进程
|
||||
if handle::Handle::global().has_core_process() {
|
||||
// 检查是否持有文件锁,确保是由我们启动的进程
|
||||
if handle::Handle::global().has_core_lock() {
|
||||
RunningMode::Sidecar
|
||||
} else {
|
||||
// 有进程但没有文件锁,可能是外部启动的进程
|
||||
log::warn!(target: "app", "core process exists but no lock file");
|
||||
RunningMode::Sidecar // 仍返回Sidecar模式,但记录了警告
|
||||
}
|
||||
} else {
|
||||
RunningMode::NotRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查系统中是否存在同名进程
|
||||
async fn check_existing_processes(&self, process_name: &str) -> Result<Vec<u32>> {
|
||||
println!("[进程检查] 检查系统中是否存在进程: {}", process_name);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] Windows系统,使用tasklist命令");
|
||||
let output = Command::new("tasklist")
|
||||
.args(["/FO", "CSV", "/NH"])
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter(|line| line.contains(process_name))
|
||||
.filter_map(|line| {
|
||||
println!("[进程检查] 发现匹配行: {}", line);
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() >= 2 {
|
||||
let pid_str = parts[1].trim_matches('"');
|
||||
pid_str.parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] Linux系统,使用pgrep命令");
|
||||
let output = Command::new("pgrep")
|
||||
.arg("-f")
|
||||
.arg(process_name)
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
line.trim().parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] macOS系统,使用ps命令");
|
||||
let output = Command::new("ps")
|
||||
.args(["-ax", "-o", "pid,command"])
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter(|line| line.contains(process_name))
|
||||
.filter_map(|line| {
|
||||
println!("[进程检查] 发现匹配行: {}", line);
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
parts[0].parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查端口是否被占用
|
||||
async fn is_port_in_use(&self, port: u16) -> bool {
|
||||
println!("[端口检查] 检查端口 {} 是否被占用", port);
|
||||
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
match TcpSocket::new_v4() {
|
||||
Ok(socket) => {
|
||||
let addr = format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
match socket.bind(addr) {
|
||||
Ok(_) => {
|
||||
// 如果能绑定成功,说明端口未被占用
|
||||
println!("[端口检查] 端口 {} 未被占用", port);
|
||||
false
|
||||
}
|
||||
Err(_) => {
|
||||
// 绑定失败,端口已被占用
|
||||
println!("[端口检查] 端口 {} 已被占用", port);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// 创建socket失败,保守返回端口被占用
|
||||
println!("[端口检查] 创建Socket失败: {:?}, 假设端口已被占用", err);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 终止进程
|
||||
async fn terminate_process(&self, pid: u32) {
|
||||
println!("[进程终止] 尝试终止进程 PID: {}", pid);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 确保 Mihomo 和 Verge service 都在运行
|
||||
pub async fn ensure_running_core(&self) {
|
||||
if MihomoManager::global().is_mihomo_running().await.is_err() {
|
||||
log_err!(self.restart_core().await);
|
||||
}
|
||||
match is_service_running().await {
|
||||
Ok(false) => log_err!(self.restart_core().await),
|
||||
Ok(true) => {
|
||||
if MihomoManager::global().is_mihomo_running().await.is_err() {
|
||||
log_err!(self.restart_core().await);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||
pub is_exiting: Arc<RwLock<bool>>,
|
||||
pub core_process: Arc<RwLock<Option<CommandChild>>>,
|
||||
pub core_lock: Arc<RwLock<Option<File>>>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
@@ -17,6 +21,8 @@ impl Handle {
|
||||
HANDLE.get_or_init(|| Handle {
|
||||
app_handle: Arc::new(RwLock::new(None)),
|
||||
is_exiting: Arc::new(RwLock::new(false)),
|
||||
core_process: Arc::new(RwLock::new(None)),
|
||||
core_lock: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,7 +74,39 @@ impl Handle {
|
||||
*is_exiting = true;
|
||||
}
|
||||
|
||||
pub fn set_core_process(&self, process: CommandChild) {
|
||||
let mut core_process = self.core_process.write();
|
||||
*core_process = Some(process);
|
||||
}
|
||||
|
||||
pub fn take_core_process(&self) -> Option<CommandChild> {
|
||||
let mut core_process = self.core_process.write();
|
||||
core_process.take()
|
||||
}
|
||||
|
||||
/// 检查是否有运行中的核心进程
|
||||
pub fn has_core_process(&self) -> bool {
|
||||
self.core_process.read().is_some()
|
||||
}
|
||||
|
||||
pub fn is_exiting(&self) -> bool {
|
||||
*self.is_exiting.read()
|
||||
}
|
||||
|
||||
/// 设置核心文件锁
|
||||
pub fn set_core_lock(&self, file: File) {
|
||||
let mut core_lock = self.core_lock.write();
|
||||
*core_lock = Some(file);
|
||||
}
|
||||
|
||||
/// 释放核心文件锁
|
||||
pub fn release_core_lock(&self) -> Option<File> {
|
||||
let mut core_lock = self.core_lock.write();
|
||||
core_lock.take()
|
||||
}
|
||||
|
||||
/// 检查是否持有核心文件锁
|
||||
pub fn has_core_lock(&self) -> bool {
|
||||
self.core_lock.read().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::core::handle;
|
||||
use crate::{config::Config, feat, log_err};
|
||||
use crate::utils::resolve;
|
||||
use crate::{
|
||||
config::Config, core::handle, feat, log_err, module::lightweight::entry_lightweight_mode,
|
||||
utils::resolve,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::Manager;
|
||||
use tauri::{async_runtime, Manager};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
use tauri::async_runtime;
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
@@ -26,7 +26,10 @@ impl Hotkey {
|
||||
let verge = Config::verge();
|
||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
println!("Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||
println!(
|
||||
"Initializing hotkeys, global hotkey enabled: {}",
|
||||
enable_global_hotkey
|
||||
);
|
||||
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
||||
|
||||
// 如果全局热键被禁用,则不注册热键
|
||||
@@ -85,11 +88,17 @@ impl Hotkey {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
|
||||
println!("Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||
println!(
|
||||
"Attempting to register hotkey: {} for function: {}",
|
||||
hotkey, func
|
||||
);
|
||||
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
||||
|
||||
if manager.is_registered(hotkey) {
|
||||
println!("Hotkey {} was already registered, unregistering first", hotkey);
|
||||
println!(
|
||||
"Hotkey {} was already registered, unregistering first",
|
||||
hotkey
|
||||
);
|
||||
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
@@ -101,12 +110,12 @@ impl Hotkey {
|
||||
|| {
|
||||
println!("=== Hotkey Dashboard Window Operation Start ===");
|
||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
|
||||
|
||||
|
||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||
async_runtime::spawn_blocking(|| {
|
||||
println!("Toggle dashboard window visibility");
|
||||
log::info!(target: "app", "Toggle dashboard window visibility");
|
||||
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏它
|
||||
@@ -131,17 +140,20 @@ impl Hotkey {
|
||||
resolve::create_window();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
println!("=== Hotkey Dashboard Window Operation End ===");
|
||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
||||
}
|
||||
},
|
||||
}
|
||||
"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(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||
"quit" => || feat::quit(Some(0)),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => || feat::hide(),
|
||||
|
||||
_ => {
|
||||
println!("Invalid function: {}", func);
|
||||
@@ -169,19 +181,20 @@ impl Hotkey {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
println!("Executing function directly");
|
||||
log::info!(target: "app", "Executing function directly");
|
||||
|
||||
// 获取轻量模式状态和全局热键状态
|
||||
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
|
||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
// 在轻量模式下或配置了全局热键时,始终执行热键功能
|
||||
if is_lite_mode || is_enable_global_hotkey {
|
||||
|
||||
// 获取全局热键状态
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
f();
|
||||
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
let is_focused = window.is_focused().unwrap_or(false);
|
||||
|
||||
|
||||
if is_focused && is_visible {
|
||||
f();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod backup;
|
||||
pub mod clash_api;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod core;
|
||||
pub mod handle;
|
||||
|
||||
@@ -1,15 +1,82 @@
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::{env::current_exe, process::Command as StdCommand};
|
||||
use std::{collections::HashMap, env::current_exe, path::PathBuf, process::Command as StdCommand, time::{SystemTime, UNIX_EPOCH}};
|
||||
use tokio::time::Duration;
|
||||
|
||||
// Windows only
|
||||
|
||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.0.5"; // 定义所需的服务版本号
|
||||
|
||||
// 限制重装时间和次数的常量
|
||||
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
|
||||
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
|
||||
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
pub struct ServiceState {
|
||||
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
|
||||
pub install_count: u32, // 24小时内安装次数
|
||||
pub last_check_time: u64, // 上次检查时间
|
||||
pub last_error: Option<String>, // 上次错误信息
|
||||
}
|
||||
|
||||
impl ServiceState {
|
||||
// 获取当前的服务状态
|
||||
pub fn get() -> Self {
|
||||
if let Some(state) = Config::verge().latest().service_state.clone() {
|
||||
return state;
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 保存服务状态
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config = Config::verge();
|
||||
let mut latest = config.latest().clone();
|
||||
latest.service_state = Some(self.clone());
|
||||
*config.draft() = latest;
|
||||
config.apply();
|
||||
Config::verge().latest().save_file()
|
||||
}
|
||||
|
||||
// 更新安装信息
|
||||
pub fn record_install(&mut self) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// 检查是否需要重置计数器(24小时已过)
|
||||
if now - self.last_install_time > ONE_DAY_SECS {
|
||||
self.install_count = 0;
|
||||
}
|
||||
|
||||
self.last_install_time = now;
|
||||
self.install_count += 1;
|
||||
}
|
||||
|
||||
// 检查是否可以重新安装
|
||||
pub fn can_reinstall(&self) -> bool {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// 如果在冷却期内,不允许重装
|
||||
if now - self.last_install_time < REINSTALL_COOLDOWN_SECS {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果24小时内安装次数过多,也不允许
|
||||
if now - self.last_install_time < ONE_DAY_SECS && self.install_count >= MAX_REINSTALLS_PER_DAY {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ResponseBody {
|
||||
@@ -19,6 +86,12 @@ pub struct ResponseBody {
|
||||
pub log_file: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct VersionResponse {
|
||||
pub service: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct JsonResponse {
|
||||
pub code: u64,
|
||||
@@ -26,10 +99,26 @@ pub struct JsonResponse {
|
||||
pub data: Option<ResponseBody>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct VersionJsonResponse {
|
||||
pub code: u64,
|
||||
pub msg: String,
|
||||
pub data: Option<VersionResponse>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
log::info!(target:"app", "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target:"app", "service reinstall rejected: cooldown period or max attempts reached");
|
||||
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||
}
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -63,12 +152,20 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
let error = format!(
|
||||
"failed to install service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.save()?;
|
||||
bail!(error);
|
||||
}
|
||||
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -140,8 +237,23 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
|
||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
|
||||
// 获取提示文本,如果 i18n 失败则使用硬编码默认值
|
||||
let prompt = crate::utils::i18n::t("Service Administrator Prompt");
|
||||
let prompt = if prompt == "Service Administrator Prompt" {
|
||||
if Config::verge().latest().language.as_deref() == Some("zh")
|
||||
|| Config::verge().latest().language.is_none()
|
||||
{
|
||||
"Clash Verge 需要使用管理员权限来重新安装系统服务"
|
||||
} else {
|
||||
"Clash Verge needs administrator privileges to reinstall the system service"
|
||||
}
|
||||
} else {
|
||||
&prompt
|
||||
};
|
||||
|
||||
let command = format!(
|
||||
r#"do shell script "sudo '{uninstall_shell}' && sudo '{install_shell}'" with administrator privileges"#
|
||||
r#"do shell script "sudo '{uninstall_shell}' && sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
|
||||
log::debug!(target: "app", "command: {}", command);
|
||||
@@ -177,8 +289,78 @@ pub async fn check_service() -> Result<JsonResponse> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
/// check the service version
|
||||
pub async fn check_service_version() -> Result<String> {
|
||||
let url = format!("{SERVICE_URL}/version");
|
||||
let response = reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to connect to the Clash Verge Service")?
|
||||
.json::<VersionJsonResponse>()
|
||||
.await
|
||||
.context("failed to parse the Clash Verge Service version response")?;
|
||||
|
||||
match response.data {
|
||||
Some(data) => Ok(data.version),
|
||||
None => bail!("service version not found in response"),
|
||||
}
|
||||
}
|
||||
|
||||
/// check if service needs to be reinstalled
|
||||
pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 获取当前服务状态
|
||||
let service_state = ServiceState::get();
|
||||
|
||||
// 首先检查是否在冷却期或超过重装次数限制
|
||||
if !service_state.can_reinstall() {
|
||||
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);
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
// 打印版本字符串的原始字节,确认没有隐藏字符
|
||||
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配,无需重装");
|
||||
}
|
||||
|
||||
needs_reinstall
|
||||
},
|
||||
Err(err) => {
|
||||
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
|
||||
match is_service_running().await {
|
||||
Ok(true) => {
|
||||
log::info!(target: "app", "service is running but version check failed: {}", err);
|
||||
false // 服务在运行,不需要重装
|
||||
}
|
||||
_ => {
|
||||
log::info!(target: "app", "service is not running or unavailable");
|
||||
true // 服务不可用,需要重装
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 尝试使用现有服务启动核心,不进行重装
|
||||
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target:"app", "attempting to start core with existing service");
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
|
||||
@@ -189,6 +371,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
let config_dir = dirs::path_to_str(&config_dir)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
let config_dir = &(config_dir.replace("/verge-mihomo", "") + "/resources");
|
||||
|
||||
let log_path = dirs::service_log_file()?;
|
||||
let log_path = dirs::path_to_str(&log_path)?;
|
||||
@@ -217,6 +401,106 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target: "app", "正在尝试通过服务启动核心");
|
||||
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
// 通过字节比较确保完全匹配
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
false // 版本不匹配
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配");
|
||||
true // 版本匹配
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||
false // 无法获取版本
|
||||
}
|
||||
};
|
||||
|
||||
// 先尝试直接启动服务,如果服务可用且版本匹配
|
||||
if version_check {
|
||||
if let Ok(true) = is_service_running().await {
|
||||
// 服务正在运行且版本匹配,直接使用
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 强制执行版本检查,如果版本不匹配则重装
|
||||
if !version_check {
|
||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||
|
||||
// 获取服务状态,检查是否可以重装
|
||||
let service_state = ServiceState::get();
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target: "app", "由于限制无法重装服务");
|
||||
// 尝试直接启动,即使版本不匹配
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试重装
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
|
||||
// 尝试使用现有服务
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 重装成功,尝试启动
|
||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
match check_service().await {
|
||||
Ok(_) => {
|
||||
// 服务可访问但可能没有运行核心,尝试直接启动
|
||||
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务不可用或启动失败,检查是否需要重装
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
|
||||
// 尝试重装
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
// 重装后再次尝试启动
|
||||
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
||||
start_with_existing_service(config_file).await
|
||||
} else {
|
||||
// 不需要或不能重装,返回错误
|
||||
log::warn!(target: "app", "服务不可用且无法重装");
|
||||
bail!("Service is not available and cannot be reinstalled at this time")
|
||||
}
|
||||
}
|
||||
|
||||
/// stop the clash by service
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
let url = format!("{SERVICE_URL}/stop_clash");
|
||||
@@ -230,3 +514,38 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查服务是否正在运行
|
||||
pub async fn is_service_running() -> Result<bool> {
|
||||
let resp = check_service().await?;
|
||||
|
||||
// 检查服务状态码和消息
|
||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 强制重装服务(用于UI中的修复服务按钮)
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
|
||||
// 创建默认服务状态(重置所有限制)
|
||||
let service_state = ServiceState::default();
|
||||
service_state.save()?;
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
|
||||
// 执行重装
|
||||
match reinstall_service().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::core::handle::Handle;
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle::Handle,
|
||||
log_err,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -126,8 +126,7 @@ impl Sysopt {
|
||||
if !sys_enable {
|
||||
return self.reset_sysproxy().await;
|
||||
}
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use anyhow::bail;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
@@ -185,8 +184,7 @@ impl Sysopt {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use anyhow::bail;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
@@ -222,15 +220,50 @@ impl Sysopt {
|
||||
let enable = enable.unwrap_or(false);
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
println!("enable: {}", enable);
|
||||
|
||||
log::info!(target: "app", "Setting auto launch to: {}", enable);
|
||||
|
||||
match enable {
|
||||
true => log_err!(autostart_manager.enable()),
|
||||
false => log_err!(autostart_manager.disable()),
|
||||
true => {
|
||||
let result = autostart_manager.enable();
|
||||
if let Err(ref e) = result {
|
||||
log::error!(target: "app", "Failed to enable auto launch: {}", e);
|
||||
} else {
|
||||
log::info!(target: "app", "Auto launch enabled successfully");
|
||||
}
|
||||
log_err!(result)
|
||||
},
|
||||
false => {
|
||||
let result = autostart_manager.disable();
|
||||
if let Err(ref e) = result {
|
||||
log::error!(target: "app", "Failed to disable auto launch: {}", e);
|
||||
} else {
|
||||
log::info!(target: "app", "Auto launch disabled successfully");
|
||||
}
|
||||
log_err!(result)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前自启动的实际状态
|
||||
pub fn get_launch_status(&self) -> Result<bool> {
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {}", status);
|
||||
Ok(status)
|
||||
},
|
||||
Err(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();
|
||||
|
||||
@@ -305,8 +338,7 @@ impl Sysopt {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
|
||||
+203
-82
@@ -1,24 +1,32 @@
|
||||
use crate::config::Config;
|
||||
use crate::feat;
|
||||
use crate::core::CoreManager;
|
||||
use crate::{config::Config, core::CoreManager, feat};
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
type TaskID = u64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimerTask {
|
||||
pub task_id: TaskID,
|
||||
pub interval_minutes: u64,
|
||||
#[allow(unused)]
|
||||
pub last_run: i64, // Timestamp of last execution
|
||||
}
|
||||
|
||||
pub struct Timer {
|
||||
/// cron manager
|
||||
delay_timer: Arc<Mutex<DelayTimer>>,
|
||||
pub delay_timer: Arc<RwLock<DelayTimer>>,
|
||||
|
||||
/// save the current state
|
||||
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
||||
/// save the current state - using RwLock for better read concurrency
|
||||
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
||||
|
||||
/// increment id
|
||||
timer_count: Arc<Mutex<TaskID>>,
|
||||
/// increment id - kept as mutex since it's just a counter
|
||||
pub timer_count: Arc<Mutex<TaskID>>,
|
||||
|
||||
/// Flag to mark if timer is initialized - atomic for better performance
|
||||
pub initialized: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
@@ -26,68 +34,145 @@ impl Timer {
|
||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||
|
||||
TIMER.get_or_init(|| Timer {
|
||||
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
timer_count: Arc::new(Mutex::new(1)),
|
||||
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// restore timer
|
||||
/// Initialize timer with better error handling and atomic operations
|
||||
pub fn init(&self) -> Result<()> {
|
||||
self.refresh()?;
|
||||
// Use compare_exchange for thread-safe initialization check
|
||||
if self
|
||||
.initialized
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
log::debug!(target: "app", "Timer already initialized, skipping...");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!(target: "app", "Initializing timer...");
|
||||
|
||||
// Initialize timer tasks
|
||||
if let Err(e) = self.refresh() {
|
||||
// Reset initialization flag on error
|
||||
self.initialized
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
log::error!(target: "app", "Failed to initialize timer: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let cur_timestamp = chrono::Local::now().timestamp();
|
||||
|
||||
let timer_map = self.timer_map.lock();
|
||||
let delay_timer = self.delay_timer.lock();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
// Collect profiles that need immediate update
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
// mins to seconds
|
||||
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
||||
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||
let updated = item.updated? as i64;
|
||||
let uid = item.uid.as_ref()?;
|
||||
|
||||
if interval > 0 && cur_timestamp - updated >= interval {
|
||||
Some(item)
|
||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||
Some(uid.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|item| {
|
||||
if let Some(uid) = item.uid.as_ref() {
|
||||
if let Some((task_id, _)) = timer_map.get(uid) {
|
||||
crate::log_err!(delay_timer.advance_task(*task_id));
|
||||
}
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Advance tasks outside of locks to minimize lock contention
|
||||
if !profiles_to_update.is_empty() {
|
||||
let timer_map = self.timer_map.read();
|
||||
let delay_timer = self.delay_timer.write();
|
||||
|
||||
for uid in profiles_to_update {
|
||||
if let Some(task) = timer_map.get(&uid) {
|
||||
log::info!(target: "app", "Advancing task for uid: {}", uid);
|
||||
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
||||
log::warn!(target: "app", "Failed to advance task {}: {}", uid, e);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(target: "app", "Timer initialization completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Correctly update all cron tasks
|
||||
/// Refresh timer tasks with better error handling
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
// Generate diff outside of lock to minimize lock contention
|
||||
let diff_map = self.gen_diff();
|
||||
|
||||
let mut timer_map = self.timer_map.lock();
|
||||
let mut delay_timer = self.delay_timer.lock();
|
||||
if diff_map.is_empty() {
|
||||
log::debug!(target: "app", "No timer changes needed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (uid, diff) in diff_map.into_iter() {
|
||||
log::info!(target: "app", "Refreshing {} timer tasks", diff_map.len());
|
||||
|
||||
// Apply changes while holding locks
|
||||
let mut timer_map = self.timer_map.write();
|
||||
let mut delay_timer = self.delay_timer.write();
|
||||
|
||||
for (uid, diff) in diff_map {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
let _ = timer_map.remove(&uid);
|
||||
crate::log_err!(delay_timer.remove_task(tid));
|
||||
timer_map.remove(&uid);
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
log::warn!(target: "app", "Failed to remove task {} for uid {}: {}", tid, uid, e);
|
||||
} else {
|
||||
log::debug!(target: "app", "Removed task {} for uid {}", tid, uid);
|
||||
}
|
||||
}
|
||||
DiffFlag::Add(tid, val) => {
|
||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||
DiffFlag::Add(tid, interval) => {
|
||||
let task = TimerTask {
|
||||
task_id: tid,
|
||||
interval_minutes: interval,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
|
||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||
log::error!(target: "app", "Failed to add task for uid {}: {}", uid, e);
|
||||
timer_map.remove(&uid); // Rollback on failure
|
||||
} else {
|
||||
log::debug!(target: "app", "Added task {} for uid {}", tid, uid);
|
||||
}
|
||||
}
|
||||
DiffFlag::Mod(tid, val) => {
|
||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||
crate::log_err!(delay_timer.remove_task(tid));
|
||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||
DiffFlag::Mod(tid, interval) => {
|
||||
// Remove old task first
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
log::warn!(target: "app", "Failed to remove old task {} for uid {}: {}", tid, uid, e);
|
||||
}
|
||||
|
||||
// Then add the new one
|
||||
let task = TimerTask {
|
||||
task_id: tid,
|
||||
interval_minutes: interval,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(uid.clone(), task);
|
||||
|
||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||
log::error!(target: "app", "Failed to update task for uid {}: {}", uid, e);
|
||||
timer_map.remove(&uid); // Rollback on failure
|
||||
} else {
|
||||
log::debug!(target: "app", "Updated task {} for uid {}", tid, uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,18 +180,17 @@ impl Timer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// generate a uid -> update_interval map
|
||||
/// Generate map of profile UIDs to update intervals
|
||||
fn gen_map(&self) -> HashMap<String, u64> {
|
||||
let mut new_map = HashMap::new();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
for item in items.iter() {
|
||||
if item.option.is_some() {
|
||||
let option = item.option.as_ref().unwrap();
|
||||
let interval = option.update_interval.unwrap_or(0);
|
||||
|
||||
if interval > 0 {
|
||||
new_map.insert(item.uid.clone().unwrap(), interval);
|
||||
if let Some(option) = item.option.as_ref() {
|
||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
||||
if interval > 0 {
|
||||
new_map.insert(uid.clone(), interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,39 +199,50 @@ impl Timer {
|
||||
new_map
|
||||
}
|
||||
|
||||
/// generate the diff map for refresh
|
||||
/// Generate differences between current and new timer configuration
|
||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||
let mut diff_map = HashMap::new();
|
||||
|
||||
let timer_map = self.timer_map.lock();
|
||||
|
||||
let new_map = self.gen_map();
|
||||
let cur_map = &timer_map;
|
||||
|
||||
cur_map.iter().for_each(|(uid, (tid, val))| {
|
||||
let new_val = new_map.get(uid).unwrap_or(&0);
|
||||
// Read lock for comparing current state
|
||||
let timer_map = self.timer_map.read();
|
||||
|
||||
if *new_val == 0 {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
||||
} else if new_val != val {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
||||
// Find tasks to modify or delete
|
||||
for (uid, task) in timer_map.iter() {
|
||||
match new_map.get(uid) {
|
||||
Some(&interval) if interval != task.interval_minutes => {
|
||||
// Task exists but interval changed
|
||||
diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval));
|
||||
}
|
||||
None => {
|
||||
// Task no longer needed
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
|
||||
}
|
||||
_ => {
|
||||
// Task exists with same interval, no change needed
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut count = self.timer_count.lock();
|
||||
// Find new tasks to add
|
||||
let mut next_id = *self.timer_count.lock();
|
||||
|
||||
new_map.iter().for_each(|(uid, val)| {
|
||||
if cur_map.get(uid).is_none() {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
||||
|
||||
*count += 1;
|
||||
for (uid, &interval) in new_map.iter() {
|
||||
if !timer_map.contains_key(uid) {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval));
|
||||
next_id += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update counter only if we added new tasks
|
||||
if next_id > *self.timer_count.lock() {
|
||||
*self.timer_count.lock() = next_id;
|
||||
}
|
||||
|
||||
diff_map
|
||||
}
|
||||
|
||||
/// add a cron task
|
||||
/// Add a timer task with better error handling
|
||||
fn add_task(
|
||||
&self,
|
||||
delay_timer: &mut DelayTimer,
|
||||
@@ -155,12 +250,19 @@ impl Timer {
|
||||
tid: TaskID,
|
||||
minutes: u64,
|
||||
) -> Result<()> {
|
||||
log::info!(target: "app", "Adding task: uid={}, id={}, interval={}min", uid, tid, minutes);
|
||||
|
||||
// Create a task with reasonable retries and backoff
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(tid)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_repeated_by_minutes(minutes)
|
||||
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
||||
.spawn_async_routine(move || {
|
||||
let uid = uid.clone();
|
||||
async move {
|
||||
Self::async_task(uid).await;
|
||||
}
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
delay_timer
|
||||
@@ -170,19 +272,38 @@ impl Timer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// the task runner
|
||||
/// Async task with better error handling and logging
|
||||
async fn async_task(uid: String) {
|
||||
log::info!(target: "app", "running timer task `{uid}`");
|
||||
|
||||
// 使用更轻量级的更新方式
|
||||
if let Err(e) = feat::update_profile(uid.clone(), None).await {
|
||||
log::error!(target: "app", "timer task update error: {}", e);
|
||||
return;
|
||||
}
|
||||
let task_start = std::time::Instant::now();
|
||||
log::info!(target: "app", "Running timer task for profile: {}", uid);
|
||||
|
||||
// 只有更新成功后才刷新配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
log::error!(target: "app", "timer task refresh error: {}", e);
|
||||
// Update profile
|
||||
let profile_result = feat::update_profile(uid.clone(), None).await;
|
||||
|
||||
match profile_result {
|
||||
Ok(_) => {
|
||||
// Update configuration
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
let duration = task_start.elapsed().as_millis();
|
||||
log::info!(
|
||||
target: "app",
|
||||
"Timer task completed successfully for uid: {} (took {}ms)",
|
||||
uid, duration
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
target: "app",
|
||||
"Failed to refresh config after profile update for uid {}: {}",
|
||||
uid, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to update profile uid {}: {}", uid, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
-77
@@ -1,13 +1,14 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::core::clash_api::Rate;
|
||||
use crate::{
|
||||
cmds,
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, resolve,
|
||||
utils::resolve::VERSION,
|
||||
utils::{dirs, i18n::t},
|
||||
feat,
|
||||
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
|
||||
resolve,
|
||||
utils::{dirs, i18n::t, resolve::VERSION},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -20,13 +21,15 @@ use parking_lot::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::hash::{Hash, Hasher};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use tauri::menu::{CheckMenuItem, IsMenuItem};
|
||||
use tauri::AppHandle;
|
||||
use tauri::{
|
||||
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
|
||||
Wry,
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconEvent},
|
||||
App, AppHandle, Wry,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio::sync::broadcast;
|
||||
@@ -37,6 +40,9 @@ pub struct Tray {
|
||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub icon_hash: Arc<Mutex<Option<u64>>>,
|
||||
pub icon_cache: Arc<Mutex<Option<Vec<u8>>>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -51,6 +57,9 @@ impl Tray {
|
||||
speed_rate: Arc::new(Mutex::new(None)),
|
||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||
is_subscribed: Arc::new(RwLock::new(false)),
|
||||
icon_hash: Arc::new(Mutex::new(None)),
|
||||
icon_cache: Arc::new(Mutex::new(None)),
|
||||
rate_cache: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -66,19 +75,27 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_systray(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_incon_id = TrayIconId::new("main");
|
||||
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
|
||||
pub fn create_systray(&self, app: &App) -> Result<()> {
|
||||
let mut builder = TrayIconBuilder::with_id("main")
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.icon_as_template(false);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
tray.set_show_menu_on_left_click(false)?;
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
if tray_event.as_str() != "tray_menu" {
|
||||
builder = builder.show_menu_on_left_click(false);
|
||||
}
|
||||
}
|
||||
|
||||
let tray = builder.build(app)?;
|
||||
|
||||
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);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
@@ -87,22 +104,7 @@ impl Tray {
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"main_window" => resolve::create_window(),
|
||||
_ => {}
|
||||
}
|
||||
@@ -112,6 +114,19 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘点击行为
|
||||
pub fn update_click_behavior(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
match tray_event.as_str() {
|
||||
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
|
||||
_ => tray.set_show_menu_on_left_click(false)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘菜单
|
||||
pub fn update_menu(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
@@ -130,7 +145,7 @@ impl Tray {
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.data()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or(Vec::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
@@ -224,34 +239,66 @@ impl Tray {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let is_colorful = tray_icon == "colorful";
|
||||
|
||||
// 处理图标和速率
|
||||
let final_icon_bytes = if enable_tray_speed {
|
||||
let rate = rate.or_else(|| {
|
||||
self.speed_rate
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
||||
});
|
||||
|
||||
// 使用新的方法渲染图标和速率
|
||||
SpeedRate::add_speed_text(icon_bytes, rate)?
|
||||
} else {
|
||||
icon_bytes
|
||||
|
||||
let icon_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
icon_bytes.clone().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
// 设置系统托盘图标
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
||||
// 只对单色图标使用 template 模式
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
let mut icon_hash_guard = self.icon_hash.lock();
|
||||
let mut icon_bytes_guard = self.icon_cache.lock();
|
||||
if *icon_hash_guard != Some(icon_hash) {
|
||||
*icon_hash_guard = Some(icon_hash);
|
||||
*icon_bytes_guard = Some(icon_bytes.clone());
|
||||
}
|
||||
|
||||
if !enable_tray_speed || (!enable_tray_speed && !enable_tray_icon) {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(
|
||||
&(*icon_bytes_guard).clone().unwrap(),
|
||||
)?));
|
||||
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(rate) = guard.as_ref().unwrap().get_curent_rate() {
|
||||
Some(rate)
|
||||
} 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_guard.as_ref().unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rate = rate_guard.as_ref();
|
||||
let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap();
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
|
||||
Ok(())
|
||||
{
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
@@ -395,21 +442,24 @@ fn create_tray_menu(
|
||||
.collect::<std::collections::HashMap<String, String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let is_current_profile = Config::profiles().data().is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
&format!("profiles_{}", profile_uid),
|
||||
t(&profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap()
|
||||
}).collect();
|
||||
let is_current_profile = Config::profiles()
|
||||
.data()
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("profiles_{}", profile_uid),
|
||||
t(profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
@@ -455,12 +505,13 @@ fn create_tray_menu(
|
||||
.unwrap();
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
app_handle,
|
||||
"profiles",
|
||||
t("Profiles"),
|
||||
true,
|
||||
true,
|
||||
&profile_menu_items,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -482,6 +533,15 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let lighteweight_mode = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"entry_lightweight_mode",
|
||||
t("LightWeight Mode"),
|
||||
true,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_env =
|
||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||
|
||||
@@ -574,6 +634,8 @@ fn create_tray_menu(
|
||||
separator,
|
||||
system_proxy,
|
||||
tun_mode,
|
||||
separator,
|
||||
lighteweight_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
@@ -594,17 +656,18 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
"open_window" => resolve::create_window(),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"open_app_dir" => crate::log_err!(cmd::open_app_dir()),
|
||||
"open_core_dir" => crate::log_err!(cmd::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||
"quit" => {
|
||||
println!("quit");
|
||||
feat::quit(Some(0));
|
||||
},
|
||||
}
|
||||
id if id.starts_with("profiles_") => {
|
||||
let profile_index = &id["profiles_".len()..];
|
||||
feat::toggle_proxy_profile(profile_index.into());
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use crate::core::clash_api::{get_traffic_ws_url, Rate};
|
||||
use crate::utils::help::format_bytes_speed;
|
||||
use crate::{
|
||||
module::mihomo::{MihomoManager, Rate},
|
||||
utils::help::format_bytes_speed,
|
||||
};
|
||||
use ab_glyph::FontArc;
|
||||
use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use image::{Rgba, GenericImageView, RgbaImage};
|
||||
use image::{GenericImageView, Rgba, RgbaImage};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use parking_lot::Mutex;
|
||||
use rusttype::{Font, Scale};
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
use tokio_tungstenite::tungstenite::{http, Message};
|
||||
use tungstenite::client::IntoClientRequest;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
// 移除 base_image,不再缓存原始图像
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
@@ -29,7 +29,7 @@ impl SpeedRate {
|
||||
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;
|
||||
@@ -37,11 +37,20 @@ impl SpeedRate {
|
||||
|
||||
let (current, previous) = &mut *rates;
|
||||
|
||||
// 如果速率变化不大(小于10%),则不更新
|
||||
let should_update = {
|
||||
let up_change = (current.up as f64 - up as f64).abs() / (current.up as f64 + 1.0);
|
||||
let down_change = (current.down as f64 - down as f64).abs() / (current.down as f64 + 1.0);
|
||||
up_change > 0.1 || down_change > 0.1
|
||||
// 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 {
|
||||
@@ -67,67 +76,96 @@ impl SpeedRate {
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
||||
|
||||
// 加载原始图标
|
||||
let icon_image = image::load_from_memory(&icon_bytes)?;
|
||||
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
||||
|
||||
pub fn add_speed_text<'a>(
|
||||
icon_bytes: Option<&'a Vec<u8>>,
|
||||
rate: Option<&'a 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 {
|
||||
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 is_colorful = !crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||
|
||||
let is_colorful = if let Some(bytes) = icon_bytes {
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 增加文本宽度和间距
|
||||
let text_width = 580; // 文本区域宽度
|
||||
let total_width = icon_width + text_width;
|
||||
|
||||
let total_width = if icon_bytes.is_some() {
|
||||
if icon_width < 580 {
|
||||
icon_width + 580
|
||||
} else {
|
||||
icon_width
|
||||
}
|
||||
} else {
|
||||
580
|
||||
};
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
||||
|
||||
// 将原始图标绘制到新画布的左侧
|
||||
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);
|
||||
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 (text_color, shadow_color) = if is_colorful {
|
||||
// 彩色图标使用黑色文本和轻微白色阴影
|
||||
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 160u8]))
|
||||
(
|
||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 160u8]),
|
||||
)
|
||||
} else {
|
||||
// 单色图标使用白色文本和轻微黑色阴影
|
||||
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 120u8]))
|
||||
(
|
||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||
Rgba([0u8, 0u8, 0u8, 120u8]),
|
||||
)
|
||||
};
|
||||
|
||||
// 减小字体大小以适应文本区域
|
||||
let font = Font::try_from_bytes(include_bytes!("../../../assets/fonts/SF-Pro.ttf")).unwrap();
|
||||
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
|
||||
let scale = Scale::uniform(font_size);
|
||||
|
||||
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_bytes_speed(rate.up);
|
||||
let down_text = format_bytes_speed(rate.down);
|
||||
|
||||
|
||||
// 计算文本位置,确保垂直间距合适
|
||||
// 修改文本位置为居右显示
|
||||
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 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,
|
||||
@@ -147,7 +185,7 @@ impl SpeedRate {
|
||||
&font,
|
||||
&up_text,
|
||||
);
|
||||
|
||||
|
||||
// 绘制下行速率
|
||||
draw_text_mut(
|
||||
&mut combined_image,
|
||||
@@ -167,13 +205,12 @@ impl SpeedRate {
|
||||
&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)]
|
||||
@@ -191,9 +228,13 @@ impl Traffic {
|
||||
let stream = Box::pin(
|
||||
stream::unfold((), |_| async {
|
||||
loop {
|
||||
let ws_url = get_traffic_ws_url().unwrap();
|
||||
let (url, token) = MihomoManager::get_traffic_ws_url();
|
||||
let mut request = url.into_client_request().unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(http::header::AUTHORIZATION, token);
|
||||
|
||||
match tokio_tungstenite::connect_async(&ws_url).await {
|
||||
match tokio_tungstenite::connect_async(request).await {
|
||||
Ok((ws_stream, _)) => {
|
||||
log::info!(target: "app", "traffic ws connection established");
|
||||
return Some((
|
||||
|
||||
@@ -5,17 +5,10 @@ mod script;
|
||||
pub mod seq;
|
||||
mod tun;
|
||||
|
||||
use self::chain::*;
|
||||
use self::field::*;
|
||||
use self::merge::*;
|
||||
use self::script::*;
|
||||
use self::seq::*;
|
||||
use self::tun::*;
|
||||
use crate::config::Config;
|
||||
use crate::utils::tmpl;
|
||||
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
||||
use crate::{config::Config, utils::tmpl};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
|
||||
@@ -25,7 +18,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的订阅
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled) = {
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
@@ -34,6 +27,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||
verge.verge_socks_enabled.unwrap_or(false),
|
||||
verge.verge_http_enabled.unwrap_or(false),
|
||||
verge.enable_dns_settings.unwrap_or(false),
|
||||
)
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -262,6 +256,27 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
config = use_tun(config, enable_tun).await;
|
||||
config = use_sort(config);
|
||||
|
||||
// 应用独立的DNS配置(如果启用)
|
||||
if enable_dns_settings {
|
||||
use crate::utils::dirs;
|
||||
use std::fs;
|
||||
|
||||
// 尝试读取dns_config.yaml
|
||||
if let Ok(app_dir) = dirs::app_home_dir() {
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
|
||||
if dns_path.exists() {
|
||||
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
||||
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||
// 将DNS配置合并到最终配置中
|
||||
config.insert("dns".into(), dns_config.into());
|
||||
log::info!(target: "app", "apply dns_config.yaml");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys);
|
||||
exists_keys = exists_set.into_iter().collect();
|
||||
|
||||
@@ -85,6 +85,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use serde_yaml::Value;
|
||||
|
||||
#[test]
|
||||
@@ -107,7 +108,7 @@ proxy-groups:
|
||||
- "proxy1"
|
||||
"#;
|
||||
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
||||
|
||||
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
@@ -120,16 +121,32 @@ proxy-groups:
|
||||
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
||||
assert_eq!(proxies.len(), 1);
|
||||
assert_eq!(
|
||||
proxies[0].as_mapping().unwrap().get("name").unwrap().as_str().unwrap(),
|
||||
proxies[0]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.get("name")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
"proxy2"
|
||||
);
|
||||
|
||||
// Check if proxy1 is removed from all groups
|
||||
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
||||
let group1_proxies = groups[0].as_mapping().unwrap()
|
||||
.get("proxies").unwrap().as_sequence().unwrap();
|
||||
let group2_proxies = groups[1].as_mapping().unwrap()
|
||||
.get("proxies").unwrap().as_sequence().unwrap();
|
||||
let group1_proxies = groups[0]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.get("proxies")
|
||||
.unwrap()
|
||||
.as_sequence()
|
||||
.unwrap();
|
||||
let group2_proxies = groups[1]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.get("proxies")
|
||||
.unwrap()
|
||||
.as_sequence()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(group1_proxies.len(), 1);
|
||||
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
||||
|
||||
@@ -24,34 +24,36 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
let dns_key = Value::from("dns");
|
||||
let dns_val = config.get(&dns_key);
|
||||
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
let ipv6_key = Value::from("ipv6");
|
||||
let ipv6_val = config
|
||||
.get(&ipv6_key)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable {
|
||||
// 读取DNS配置
|
||||
let dns_key = Value::from("dns");
|
||||
let dns_val = config.get(&dns_key);
|
||||
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
let ipv6_key = Value::from("ipv6");
|
||||
let ipv6_val = config
|
||||
.get(&ipv6_key)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
// 检查现有的 enhanced-mode 设置
|
||||
let current_mode = dns_val
|
||||
.get(&Value::from("enhanced-mode"))
|
||||
.get(Value::from("enhanced-mode"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("fake-ip");
|
||||
|
||||
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
if !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
||||
|
||||
if !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||
}
|
||||
|
||||
if !dns_val.contains_key(&Value::from("fake-ip-range")) {
|
||||
|
||||
if !dns_val.contains_key(Value::from("fake-ip-range")) {
|
||||
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||
}
|
||||
|
||||
@@ -61,42 +63,18 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 当TUN启用时,将修改后的DNS配置写回
|
||||
revise!(config, "dns", dns_val);
|
||||
} else {
|
||||
revise!(
|
||||
dns_val,
|
||||
"enable",
|
||||
dns_val
|
||||
.get("enable")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true)
|
||||
);
|
||||
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
revise!(
|
||||
dns_val,
|
||||
"enhanced-mode",
|
||||
dns_val
|
||||
.get("enhanced-mode")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("redir-host")
|
||||
);
|
||||
|
||||
revise!(
|
||||
dns_val,
|
||||
"fake-ip-range",
|
||||
dns_val
|
||||
.get("fake-ip-range")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("198.18.0.1/16")
|
||||
);
|
||||
|
||||
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
}
|
||||
|
||||
// 更新TUN配置
|
||||
revise!(tun_val, "enable", enable);
|
||||
revise!(config, "tun", tun_val);
|
||||
revise!(config, "dns", dns_val);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -1,626 +0,0 @@
|
||||
//!
|
||||
//! feat mod 里的函数主要用于
|
||||
//! - hotkey 快捷键
|
||||
//! - timer 定时器
|
||||
//! - cmds 页面调用
|
||||
//!
|
||||
use crate::cmds;
|
||||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::dirs::app_home_dir;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
use std::env;
|
||||
|
||||
// 打开面板
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
println!("Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window");
|
||||
log::info!(target: "app", "Found existing window");
|
||||
|
||||
// 如果窗口存在,则切换其显示状态
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
println!("Window visibility status: {}", visible);
|
||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||
|
||||
if visible {
|
||||
println!("Attempting to hide window");
|
||||
log::info!(target: "app", "Attempting to hide window");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
println!("Attempting to show and focus window");
|
||||
log::info!(target: "app", "Attempting to show and focus window");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get window visibility: {:?}", e);
|
||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No existing window found, creating new window");
|
||||
log::info!(target: "app", "No existing window found, creating new window");
|
||||
resolve::create_window();
|
||||
}
|
||||
}
|
||||
|
||||
// 重启clash
|
||||
pub fn restart_clash_core() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn restart_app() {
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
log_err!(CoreManager::global().stop_core().await);
|
||||
});
|
||||
resolve::resolve_reset();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
tauri::process::restart(&app_handle.env());
|
||||
});
|
||||
}
|
||||
|
||||
/// 设置窗口状态监控,实时保存窗口位置和大小
|
||||
pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
// 监听窗口移动事件
|
||||
let app_handle_move = app_handle_clone.clone();
|
||||
window.on_window_event(move |event| {
|
||||
match event {
|
||||
// 窗口移动时保存状态
|
||||
tauri::WindowEvent::Moved(_) => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
},
|
||||
// 窗口调整大小时保存状态
|
||||
tauri::WindowEvent::Resized(_) => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
},
|
||||
// 其他可能改变窗口状态的事件
|
||||
tauri::WindowEvent::ScaleFactorChanged { .. } => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
},
|
||||
// 窗口关闭时保存
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换模式 rule/global/direct/script mode
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
|
||||
match clash_api::patch_configs(&mapping).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
log_err!(tray::Tray::global().update_menu());
|
||||
log_err!(tray::Tray::global().update_icon(None));
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换系统代理
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match patch_verge(IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换代理文件
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
match cmds::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换tun模式
|
||||
pub fn toggle_tun_mode() {
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match patch_verge(IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn quit(code: Option<i32>) {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
handle::Handle::global().set_is_exiting();
|
||||
resolve::resolve_reset();
|
||||
log_err!(handle::Handle::global().get_window().unwrap().close());
|
||||
app_handle.exit(code.unwrap_or(0));
|
||||
}
|
||||
|
||||
/// 修改clash的订阅
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
log_err!(tray::Tray::global().update_menu());
|
||||
log_err!(tray::Tray::global().update_icon(None));
|
||||
}
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
CoreManager::global().update_config().await?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改verge的订阅
|
||||
/// 一般都是一个个的修改
|
||||
pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let pac = patch.proxy_auto_config;
|
||||
let pac_content = patch.pac_file_content;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
let lite_mode = patch.enable_lite_mode;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let tray_icon: Option<String> = None;
|
||||
let common_tray_icon = patch.common_tray_icon;
|
||||
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
|
||||
let tun_tray_icon = patch.tun_tray_icon;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = patch.verge_redir_enabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_port = patch.verge_redir_port;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = patch.verge_tproxy_enabled;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_port = patch.verge_tproxy_port;
|
||||
let socks_enabled = patch.verge_socks_enabled;
|
||||
let socks_port = patch.verge_socks_port;
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
let mut should_restart_core = false;
|
||||
let mut should_update_clash_config = false;
|
||||
let mut should_update_verge_config = false;
|
||||
let mut should_update_launch = false;
|
||||
let mut should_update_sysproxy = false;
|
||||
let mut should_update_systray_icon = false;
|
||||
let mut should_update_hotkey = false;
|
||||
let mut should_update_systray_menu = false;
|
||||
let mut should_update_systray_tooltip = false;
|
||||
|
||||
if tun_mode.is_some() {
|
||||
should_update_clash_config = true;
|
||||
should_update_systray_menu = true;
|
||||
should_update_systray_tooltip = true;
|
||||
should_update_systray_icon = true;
|
||||
}
|
||||
if enable_global_hotkey.is_some() {
|
||||
should_update_verge_config = true;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
should_restart_core = true;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
should_restart_core = true;
|
||||
}
|
||||
if socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
{
|
||||
should_restart_core = true;
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
should_update_launch = true;
|
||||
}
|
||||
|
||||
if system_proxy.is_some() {
|
||||
should_update_sysproxy = true;
|
||||
should_update_systray_menu = true;
|
||||
should_update_systray_tooltip = true;
|
||||
should_update_systray_icon = true;
|
||||
}
|
||||
|
||||
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
||||
should_update_sysproxy = true;
|
||||
}
|
||||
|
||||
if language.is_some() {
|
||||
should_update_systray_menu = true;
|
||||
}
|
||||
if common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
{
|
||||
should_update_systray_icon = true;
|
||||
}
|
||||
|
||||
if patch.hotkeys.is_some() {
|
||||
should_update_hotkey = true;
|
||||
should_update_systray_menu = true;
|
||||
}
|
||||
|
||||
if enable_tray_speed.is_some() {
|
||||
should_update_systray_icon = true;
|
||||
}
|
||||
|
||||
if should_restart_core {
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if should_update_clash_config {
|
||||
CoreManager::global().update_config().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if should_update_verge_config {
|
||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if should_update_launch {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
|
||||
if should_update_sysproxy {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
|
||||
if should_update_hotkey {
|
||||
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
||||
}
|
||||
|
||||
if should_update_systray_menu {
|
||||
tray::Tray::global().update_menu()?;
|
||||
}
|
||||
|
||||
if should_update_systray_icon {
|
||||
tray::Tray::global().update_icon(None)?;
|
||||
}
|
||||
|
||||
if should_update_systray_tooltip {
|
||||
tray::Tray::global().update_tooltip()?;
|
||||
}
|
||||
|
||||
// 处理轻量模式切换
|
||||
if lite_mode.is_some() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if lite_mode.unwrap() {
|
||||
// 完全退出 webview 进程
|
||||
window.close()?; // 先关闭窗口
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
if let Some(webview) = app_handle.get_webview_window("main") {
|
||||
webview.destroy()?; // 销毁 webview 进程
|
||||
}
|
||||
} else {
|
||||
resolve::create_window(); // 重新创建窗口
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
Config::verge().data().save_file()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新某个profile
|
||||
/// 如果更新当前订阅就激活订阅
|
||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
println!("[订阅更新] {} 是远程订阅,URL: {}", uid, item.url.clone().unwrap());
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
println!("[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
|
||||
println!("[订阅更新] 更新订阅配置");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
is_current
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
println!("[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// copy env variable
|
||||
pub fn copy_clash_env() {
|
||||
// 从环境变量获取IP地址,默认127.0.0.1
|
||||
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
|
||||
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 sh =
|
||||
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
|
||||
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||
let nu: String =
|
||||
format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}");
|
||||
|
||||
let cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
let env_type = match env_type {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let default = "bash";
|
||||
#[cfg(target_os = "windows")]
|
||||
let default = "powershell";
|
||||
|
||||
default.to_string()
|
||||
}
|
||||
};
|
||||
match env_type.as_str() {
|
||||
"bash" => cliboard.write_text(sh).unwrap_or_default(),
|
||||
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
|
||||
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
|
||||
"nushell" => cliboard.write_text(nu).unwrap_or_default(),
|
||||
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn test_delay(url: String) -> Result<u32> {
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if !tun_mode {
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let request = builder
|
||||
.timeout(Duration::from_millis(10000))
|
||||
.build()?
|
||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||
let start = Instant::now();
|
||||
|
||||
let response = request.send().await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
Ok(10000u32)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
err
|
||||
})?;
|
||||
|
||||
if let Err(err) = backup::WebDavClient::global()
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
backup::WebDavClient::global()
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
||||
backup::WebDavClient::global()
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
// extract zip file
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
|
||||
log_err!(
|
||||
patch_verge(IVerge {
|
||||
webdav_url,
|
||||
webdav_username,
|
||||
webdav_password,
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
);
|
||||
// 最后删除临时文件
|
||||
fs::remove_file(backup_storage_path)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::backup,
|
||||
log_err,
|
||||
utils::dirs::app_home_dir,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
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);
|
||||
err
|
||||
})?;
|
||||
|
||||
if let Err(err) = backup::WebDavClient::global()
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete WebDAV backup
|
||||
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
backup::WebDavClient::global()
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore WebDAV backup
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
||||
backup::WebDavClient::global()
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
// extract zip file
|
||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
|
||||
log_err!(
|
||||
super::patch_verge(
|
||||
IVerge {
|
||||
webdav_url,
|
||||
webdav_username,
|
||||
webdav_password,
|
||||
..IVerge::default()
|
||||
},
|
||||
false
|
||||
)
|
||||
.await
|
||||
);
|
||||
// 最后删除临时文件
|
||||
fs::remove_file(backup_storage_path)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, tray, CoreManager},
|
||||
log_err,
|
||||
module::mihomo::MihomoManager,
|
||||
utils::resolve,
|
||||
};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use tauri::Manager;
|
||||
|
||||
/// Restart the Clash core
|
||||
pub fn restart_clash_core() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Restart the application
|
||||
pub fn restart_app() {
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
log_err!(CoreManager::global().stop_core().await);
|
||||
});
|
||||
resolve::resolve_reset();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
tauri::process::restart(&app_handle.env());
|
||||
});
|
||||
}
|
||||
|
||||
/// Change Clash mode (rule/global/direct/script)
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
// Convert YAML mapping to JSON Value
|
||||
let json_value = serde_json::json!({
|
||||
"mode": mode
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
|
||||
match MihomoManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
log_err!(tray::Tray::global().update_menu());
|
||||
log_err!(tray::Tray::global().update_icon(None));
|
||||
}
|
||||
}
|
||||
Err(err) => println!("{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Test connection delay to a URL
|
||||
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if !tun_mode {
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let request = builder
|
||||
.timeout(Duration::from_millis(10000))
|
||||
.build()?
|
||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||
let start = Instant::now();
|
||||
|
||||
let response = request.send().await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
Ok(10000u32)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::{handle, hotkey, sysopt, tray, CoreManager},
|
||||
log_err,
|
||||
module::lightweight,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
/// Patch Clash configuration
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
log_err!(tray::Tray::global().update_menu());
|
||||
log_err!(tray::Tray::global().update_icon(None));
|
||||
}
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
CoreManager::global().update_config().await?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define update flags as bitflags for better performance
|
||||
#[derive(Clone, Copy)]
|
||||
enum UpdateFlags {
|
||||
None = 0,
|
||||
RestartCore = 1 << 0,
|
||||
ClashConfig = 1 << 1,
|
||||
VergeConfig = 1 << 2,
|
||||
Launch = 1 << 3,
|
||||
SysProxy = 1 << 4,
|
||||
SystrayIcon = 1 << 5,
|
||||
Hotkey = 1 << 6,
|
||||
SystrayMenu = 1 << 7,
|
||||
SystrayTooltip = 1 << 8,
|
||||
SystrayClickBehavior = 1 << 9,
|
||||
LighteWeight = 1 << 10,
|
||||
}
|
||||
|
||||
/// Patch Verge configuration
|
||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let pac = patch.proxy_auto_config;
|
||||
let pac_content = patch.pac_file_content;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
let mixed_port = patch.verge_mixed_port;
|
||||
#[cfg(target_os = "macos")]
|
||||
let tray_icon = patch.tray_icon;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let tray_icon: Option<String> = None;
|
||||
let common_tray_icon = patch.common_tray_icon;
|
||||
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
|
||||
let tun_tray_icon = patch.tun_tray_icon;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = patch.verge_redir_enabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_port = patch.verge_redir_port;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = patch.verge_tproxy_enabled;
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_port = patch.verge_tproxy_port;
|
||||
let socks_enabled = patch.verge_socks_enabled;
|
||||
let socks_port = patch.verge_socks_port;
|
||||
let http_enabled = patch.verge_http_enabled;
|
||||
let http_port = patch.verge_port;
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
let enable_tray_icon = patch.enable_tray_icon;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
let tray_event = patch.tray_event;
|
||||
let home_cards = patch.home_cards.clone();
|
||||
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
// Initialize with no flags set
|
||||
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||
|
||||
if tun_mode.is_some() {
|
||||
update_flags |= UpdateFlags::ClashConfig as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
if enable_global_hotkey.is_some() || home_cards.is_some() {
|
||||
update_flags |= UpdateFlags::VergeConfig as i32;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if redir_enabled.is_some() || redir_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if socks_enabled.is_some()
|
||||
|| http_enabled.is_some()
|
||||
|| socks_port.is_some()
|
||||
|| http_port.is_some()
|
||||
|| mixed_port.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
if auto_launch.is_some() {
|
||||
update_flags |= UpdateFlags::Launch as i32;
|
||||
}
|
||||
|
||||
if system_proxy.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
|
||||
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
||||
update_flags |= UpdateFlags::SysProxy as i32;
|
||||
}
|
||||
|
||||
if language.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
if common_tray_icon.is_some()
|
||||
|| sysproxy_tray_icon.is_some()
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
|| enable_tray_speed.is_some()
|
||||
|| enable_tray_icon.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
|
||||
if patch.hotkeys.is_some() {
|
||||
update_flags |= UpdateFlags::Hotkey as i32;
|
||||
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||
}
|
||||
|
||||
if tray_event.is_some() {
|
||||
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
|
||||
}
|
||||
|
||||
if enable_auto_light_weight.is_some() {
|
||||
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||
}
|
||||
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
CoreManager::global().update_config().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
|
||||
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||
tray::Tray::global().update_menu()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||
tray::Tray::global().update_icon(None)?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||
tray::Tray::global().update_tooltip()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
||||
tray::Tray::global().update_click_behavior()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||
if enable_auto_light_weight.unwrap() {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
if !not_save_file {
|
||||
Config::verge().data().save_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
mod backup;
|
||||
mod clash;
|
||||
mod config;
|
||||
mod profile;
|
||||
mod proxy;
|
||||
mod window;
|
||||
|
||||
// Re-export all functions from modules
|
||||
pub use backup::*;
|
||||
pub use clash::*;
|
||||
pub use config::*;
|
||||
pub use profile::*;
|
||||
pub use proxy::*;
|
||||
pub use window::*;
|
||||
@@ -0,0 +1,84 @@
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption},
|
||||
core::{handle, CoreManager, *},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update a profile
|
||||
/// If updating current profile, activate it
|
||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
println!(
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url.clone().unwrap()
|
||||
);
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
println!("[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
|
||||
println!("[订阅更新] 更新订阅配置");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
is_current
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
println!("[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
/// Toggle system proxy on/off
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Toggle TUN mode on/off
|
||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
not_save_file.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Copy proxy environment variables to clipboard
|
||||
pub fn copy_clash_env() {
|
||||
// 从环境变量获取IP地址,默认127.0.0.1
|
||||
let clash_verge_rev_ip =
|
||||
env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
|
||||
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 cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
let env_type = match env_type {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let default = "bash";
|
||||
#[cfg(target_os = "windows")]
|
||||
let default = "powershell";
|
||||
|
||||
default.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let export_text = match env_type.as_str() {
|
||||
"bash" => format!(
|
||||
"export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"
|
||||
),
|
||||
"cmd" => format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}"),
|
||||
"powershell" => {
|
||||
format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"")
|
||||
}
|
||||
"nushell" => {
|
||||
format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}")
|
||||
}
|
||||
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
|
||||
_ => {
|
||||
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if cliboard.write_text(export_text).is_err() {
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, sysopt, CoreManager},
|
||||
module::mihomo::MihomoManager,
|
||||
utils::resolve,
|
||||
};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
println!("Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window");
|
||||
log::info!(target: "app", "Found existing window");
|
||||
|
||||
// 如果窗口存在,则切换其显示状态
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
println!("Window visibility status: {}", visible);
|
||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||
|
||||
if visible {
|
||||
println!("Attempting to hide window");
|
||||
log::info!(target: "app", "Attempting to hide window");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
println!("Attempting to show and focus window");
|
||||
log::info!(target: "app", "Attempting to show and focus window");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get window visibility: {:?}", e);
|
||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No existing window found, creating new window");
|
||||
log::info!(target: "app", "No existing window found, creating new window");
|
||||
resolve::create_window();
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup window state monitor to save window position and size in real-time
|
||||
pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
|
||||
let window = app_handle.get_webview_window("main").unwrap();
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
// 监听窗口移动事件
|
||||
let app_handle_move = app_handle_clone.clone();
|
||||
window.on_window_event(move |event| {
|
||||
match event {
|
||||
// 窗口移动时保存状态
|
||||
tauri::WindowEvent::Moved(_) => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
}
|
||||
// 窗口调整大小时保存状态
|
||||
tauri::WindowEvent::Resized(_) => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
}
|
||||
// 其他可能改变窗口状态的事件
|
||||
tauri::WindowEvent::ScaleFactorChanged { .. } => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
}
|
||||
// 窗口关闭时保存
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 优化的应用退出函数
|
||||
pub fn quit(code: Option<i32>) {
|
||||
log::debug!(target: "app", "启动退出流程");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
}
|
||||
|
||||
// 在单独线程中处理资源清理,避免阻塞主线程
|
||||
std::thread::spawn(move || {
|
||||
// 使用tokio运行时执行异步清理任务
|
||||
tauri::async_runtime::block_on(async {
|
||||
// 使用超时机制处理清理操作
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
// 1. 直接关闭TUN模式 (优先处理,通常最容易卡住)
|
||||
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||
let disable = serde_json::json!({
|
||||
"tun": {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
|
||||
// 设置1秒超时
|
||||
let _ = timeout(
|
||||
Duration::from_secs(1),
|
||||
MihomoManager::global().patch_configs(disable),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// 2. 并行处理系统代理和核心进程清理
|
||||
let proxy_future = timeout(
|
||||
Duration::from_secs(1),
|
||||
sysopt::Sysopt::global().reset_sysproxy(),
|
||||
);
|
||||
|
||||
let core_future = timeout(Duration::from_secs(1), CoreManager::global().stop_core());
|
||||
|
||||
// 同时等待两个任务完成
|
||||
let _ = futures::join!(proxy_future, core_future);
|
||||
|
||||
// 3. 处理macOS特定清理
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
|
||||
}
|
||||
});
|
||||
|
||||
// 无论清理结果如何,确保应用退出
|
||||
app_handle.exit(code.unwrap_or(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn hide() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
+187
-58
@@ -1,15 +1,85 @@
|
||||
mod cmds;
|
||||
mod cmd;
|
||||
mod config;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
mod module;
|
||||
mod utils;
|
||||
use crate::core::hotkey;
|
||||
use crate::utils::{resolve, resolve::resolve_scheme, server};
|
||||
use crate::{
|
||||
core::hotkey,
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
/// A global singleton handle to the application.
|
||||
pub struct AppHandleManager {
|
||||
inner: Mutex<Option<AppHandle>>,
|
||||
init: Once,
|
||||
}
|
||||
|
||||
impl AppHandleManager {
|
||||
/// Get the global instance of the app handle manager.
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: AppHandleManager = AppHandleManager {
|
||||
inner: Mutex::new(None),
|
||||
init: Once::new(),
|
||||
};
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Initialize the app handle manager with an app handle.
|
||||
pub fn init(&self, handle: AppHandle) {
|
||||
self.init.call_once(|| {
|
||||
let mut app_handle = self.inner.lock().unwrap();
|
||||
*app_handle = Some(handle);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the app handle if it has been initialized.
|
||||
pub fn get(&self) -> Option<AppHandle> {
|
||||
self.inner.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Get the app handle, panics if it hasn't been initialized.
|
||||
pub fn get_handle(&self) -> AppHandle {
|
||||
self.get().expect("AppHandle not initialized")
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_regular(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_accessory(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_prohibited(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub fn run() {
|
||||
// 单例检测
|
||||
let app_exists: bool = tauri::async_runtime::block_on(async move {
|
||||
@@ -29,7 +99,6 @@ pub fn run() {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let devtools = tauri_plugin_devtools::init();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
@@ -70,61 +139,82 @@ pub fn run() {
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
cmds::get_sys_proxy,
|
||||
cmds::get_auto_proxy,
|
||||
cmds::open_app_dir,
|
||||
cmds::open_logs_dir,
|
||||
cmds::open_web_url,
|
||||
cmds::open_core_dir,
|
||||
cmds::get_portable_flag,
|
||||
cmds::get_network_interfaces,
|
||||
cmds::restart_core,
|
||||
cmds::restart_app,
|
||||
cmd::get_sys_proxy,
|
||||
cmd::get_auto_proxy,
|
||||
cmd::open_app_dir,
|
||||
cmd::open_logs_dir,
|
||||
cmd::open_web_url,
|
||||
cmd::open_core_dir,
|
||||
cmd::get_portable_flag,
|
||||
cmd::get_network_interfaces,
|
||||
cmd::restart_core,
|
||||
cmd::restart_app,
|
||||
// 添加新的命令
|
||||
cmd::get_running_mode,
|
||||
cmd::install_service,
|
||||
cmd::repair_service,
|
||||
cmd::get_app_uptime,
|
||||
cmd::get_auto_launch_status,
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::patch_clash_config,
|
||||
cmds::patch_clash_mode,
|
||||
cmds::change_clash_core,
|
||||
cmds::get_runtime_config,
|
||||
cmds::get_runtime_yaml,
|
||||
cmds::get_runtime_exists,
|
||||
cmds::get_runtime_logs,
|
||||
cmds::uwp::invoke_uwp_tool,
|
||||
cmds::copy_clash_env,
|
||||
cmd::get_clash_info,
|
||||
cmd::patch_clash_config,
|
||||
cmd::patch_clash_mode,
|
||||
cmd::change_clash_core,
|
||||
cmd::get_runtime_config,
|
||||
cmd::get_runtime_yaml,
|
||||
cmd::get_runtime_exists,
|
||||
cmd::get_runtime_logs,
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::get_proxies,
|
||||
cmd::get_providers_proxies,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
cmd::check_dns_config_exists,
|
||||
cmd::get_dns_config_content,
|
||||
// verge
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
cmds::test_delay,
|
||||
cmds::get_app_dir,
|
||||
cmds::copy_icon_file,
|
||||
cmds::download_icon_cache,
|
||||
cmds::open_devtools,
|
||||
cmds::exit_app,
|
||||
cmds::get_network_interfaces_info,
|
||||
cmd::get_verge_config,
|
||||
cmd::patch_verge_config,
|
||||
cmd::test_delay,
|
||||
cmd::get_app_dir,
|
||||
cmd::copy_icon_file,
|
||||
cmd::download_icon_cache,
|
||||
cmd::open_devtools,
|
||||
cmd::exit_app,
|
||||
cmd::get_network_interfaces_info,
|
||||
// profile
|
||||
cmds::get_profiles,
|
||||
cmds::enhance_profiles,
|
||||
cmds::patch_profiles_config,
|
||||
cmds::view_profile,
|
||||
cmds::patch_profile,
|
||||
cmds::create_profile,
|
||||
cmds::import_profile,
|
||||
cmds::reorder_profile,
|
||||
cmds::update_profile,
|
||||
cmds::delete_profile,
|
||||
cmds::read_profile_file,
|
||||
cmds::save_profile_file,
|
||||
cmd::get_profiles,
|
||||
cmd::enhance_profiles,
|
||||
cmd::patch_profiles_config,
|
||||
cmd::view_profile,
|
||||
cmd::patch_profile,
|
||||
cmd::create_profile,
|
||||
cmd::import_profile,
|
||||
cmd::reorder_profile,
|
||||
cmd::update_profile,
|
||||
cmd::delete_profile,
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
// script validation
|
||||
cmds::script_validate_notice,
|
||||
cmds::validate_script_file,
|
||||
cmd::script_validate_notice,
|
||||
cmd::validate_script_file,
|
||||
// clash api
|
||||
cmds::clash_api_get_proxy_delay,
|
||||
cmd::clash_api_get_proxy_delay,
|
||||
// backup
|
||||
cmds::create_webdav_backup,
|
||||
cmds::save_webdav_config,
|
||||
cmds::list_webdav_backup,
|
||||
cmds::delete_webdav_backup,
|
||||
cmds::restore_webdav_backup,
|
||||
cmd::create_webdav_backup,
|
||||
cmd::save_webdav_config,
|
||||
cmd::list_webdav_backup,
|
||||
cmd::delete_webdav_backup,
|
||||
cmd::restore_webdav_backup,
|
||||
// export diagnostic info for issue reporting
|
||||
cmd::export_diagnostic_info,
|
||||
// get system info for display
|
||||
cmd::get_system_info,
|
||||
// media unlock checker
|
||||
cmd::get_unlock_items,
|
||||
cmd::check_media_unlock,
|
||||
// light-weight model
|
||||
cmd::entry_lightweight_mode,
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -132,11 +222,39 @@ pub fn run() {
|
||||
builder = builder.plugin(devtools);
|
||||
}
|
||||
|
||||
// Macos Application Menu
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Temporary Achived due to cannot CMD+C/V/A
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|_, e| match e {
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(window) = AppHandleManager::global()
|
||||
.get_handle()
|
||||
.get_webview_window("main")
|
||||
{
|
||||
let _ = window.set_title("Clash Verge");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::Reopen {
|
||||
has_visible_windows,
|
||||
..
|
||||
} => {
|
||||
if !has_visible_windows {
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
}
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
@@ -146,6 +264,8 @@ pub fn run() {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
@@ -158,14 +278,18 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().register("CMD+Q", "quit"));
|
||||
log_err!(hotkey::Hotkey::global().register("CMD+W", "hide"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
||||
};
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
log_err!(hotkey::Hotkey::global().init())
|
||||
}
|
||||
@@ -175,13 +299,17 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
|
||||
log_err!(hotkey::Hotkey::global().unregister("CMD+W"));
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
||||
};
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
log_err!(hotkey::Hotkey::global().reset())
|
||||
}
|
||||
@@ -191,6 +319,7 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
|
||||
log_err!(hotkey::Hotkey::global().unregister("CMD+W"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer},
|
||||
log_err, AppHandleManager,
|
||||
};
|
||||
|
||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
println!("[lightweight_mode] 开启自动轻量模式");
|
||||
log::info!(target: "app", "[lightweight_mode] 开启自动轻量模式");
|
||||
setup_window_close_listener();
|
||||
setup_webview_focus_listener();
|
||||
}
|
||||
|
||||
pub fn disable_auto_light_weight_mode() {
|
||||
println!("[lightweight_mode] 关闭自动轻量模式");
|
||||
log::info!(target: "app", "[lightweight_mode] 关闭自动轻量模式");
|
||||
let _ = cancel_light_weight_timer();
|
||||
cancel_window_close_listener();
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
}
|
||||
if let Some(webview) = window.get_webview_window("main") {
|
||||
let _ = webview.destroy();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
println!("[lightweight_mode] 轻量模式已开启");
|
||||
log::info!(target: "app", "[lightweight_mode] 轻量模式已开启");
|
||||
}
|
||||
let _ = cancel_light_weight_timer();
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() -> u32 {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||
let _ = setup_light_weight_timer();
|
||||
println!("[lightweight_mode] 监听到关闭请求,开始轻量模式计时");
|
||||
log::info!(target: "app", "[lightweight_mode] 监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() -> u32 {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let handler = window.listen("tauri://focus", move |_event| {
|
||||
log_err!(cancel_light_weight_timer());
|
||||
println!("[lightweight_mode] 监听到窗口获得焦点,取消轻量模式计时");
|
||||
log::info!(target: "app", "[lightweight_mode] 监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
window.unlisten(setup_window_close_listener());
|
||||
println!("[lightweight_mode] 取消了窗口关闭监听");
|
||||
log::info!(target: "app", "[lightweight_mode] 取消了窗口关闭监听");
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init()?;
|
||||
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
let mut timer_count = Timer::global().timer_count.lock();
|
||||
|
||||
let task_id = *timer_count;
|
||||
*timer_count += 1;
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.latest()
|
||||
.auto_light_weight_minutes
|
||||
.clone()
|
||||
.unwrap_or(10);
|
||||
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(task_id)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_once_by_minutes(once_by_minutes)
|
||||
.spawn_async_routine(move || async move {
|
||||
println!("[lightweight_mode] 计时器到期,开始进入轻量模式");
|
||||
log::info!(target: "app",
|
||||
"[lightweight_mode] 计时器到期,开始进入轻量模式"
|
||||
);
|
||||
entry_lightweight_mode();
|
||||
})
|
||||
.context("failed to create light weight timer task")?;
|
||||
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add light weight timer task")?;
|
||||
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
task_id,
|
||||
interval_minutes: once_by_minutes,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||
|
||||
println!(
|
||||
"[lightweight_mode] 轻量模式计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||
once_by_minutes
|
||||
);
|
||||
log::info!(target: "app",
|
||||
"[lightweight_mode] 轻量模式计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||
once_by_minutes
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_light_weight_timer() -> Result<()> {
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
|
||||
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
||||
delay_timer
|
||||
.remove_task(task.task_id)
|
||||
.context("failed to remove light weight timer task")?;
|
||||
println!("[lightweight_mode] 轻量模式计时器已取消");
|
||||
log::info!(target: "app", "[lightweight_mode] 轻量模式计时器已取消");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use crate::config::Config;
|
||||
use mihomo_api;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use std::sync::Mutex;
|
||||
use tauri::http::{HeaderMap, HeaderValue};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tokio_tungstenite::tungstenite::http;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Rate {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
pub struct MihomoManager {
|
||||
mihomo: Mutex<OnceCell<mihomo_api::MihomoManager>>,
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
fn __global() -> &'static MihomoManager {
|
||||
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
|
||||
mihomo: Mutex::new(OnceCell::new()),
|
||||
});
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
pub fn global() -> mihomo_api::MihomoManager {
|
||||
let instance = MihomoManager::__global();
|
||||
let (current_server, headers) = MihomoManager::get_clash_client_info().unwrap();
|
||||
|
||||
let lock = instance.mihomo.lock().unwrap();
|
||||
if let Some(mihomo) = lock.get() {
|
||||
if mihomo.get_mihomo_server() == current_server {
|
||||
return mihomo.clone();
|
||||
}
|
||||
}
|
||||
|
||||
lock.set(mihomo_api::MihomoManager::new(current_server, headers))
|
||||
.ok();
|
||||
lock.get().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
let server = format!("http://{}", client.server);
|
||||
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();
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Some((server, headers))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
||||
let (url, headers) = MihomoManager::get_clash_client_info().unwrap();
|
||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
||||
let auth = headers
|
||||
.get("Authorization")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let token = http::header::HeaderValue::from_str(&auth).unwrap();
|
||||
(ws_url, token)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod mihomo;
|
||||
pub mod sysinfo;
|
||||
pub mod lightweight;
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::core::{handle, CoreManager};
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use sysinfo::System;
|
||||
|
||||
pub struct PlatformSpecification {
|
||||
system_name: String,
|
||||
system_version: String,
|
||||
system_kernel_version: String,
|
||||
system_arch: String,
|
||||
verge_version: String,
|
||||
running_mode: String,
|
||||
}
|
||||
|
||||
impl Debug for PlatformSpecification {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}",
|
||||
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformSpecification {
|
||||
pub fn new() -> Self {
|
||||
let system_name = System::name().unwrap_or("Null".into());
|
||||
let system_version = System::long_os_version().unwrap_or("Null".into());
|
||||
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
||||
let system_arch = System::cpu_arch();
|
||||
|
||||
let handler = handle::Handle::global().app_handle().unwrap();
|
||||
let config = handler.config();
|
||||
let verge_version = config.version.clone().unwrap_or("Null".into());
|
||||
|
||||
// Get running mode asynchronously
|
||||
let running_mode = tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
match CoreManager::global().get_running_mode().await {
|
||||
crate::core::RunningMode::Service => "Service".to_string(),
|
||||
crate::core::RunningMode::Sidecar => "Sidecar".to_string(),
|
||||
crate::core::RunningMode::NotRunning => "Not Running".to_string(),
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
system_name,
|
||||
system_version,
|
||||
system_kernel_version,
|
||||
system_arch,
|
||||
verge_version,
|
||||
running_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::core::handle;
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, path::PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
@@ -137,7 +136,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
|
||||
} else {
|
||||
// Generate and save new key
|
||||
let mut key = vec![0u8; 32];
|
||||
getrandom::getrandom(&mut key)?;
|
||||
getrandom::fill(&mut key)?;
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = key_path.parent() {
|
||||
|
||||
@@ -205,10 +205,11 @@ 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"
|
||||
/// ```
|
||||
/// assert_eq!(format_bytes_speed(1000), "1000B/s");
|
||||
/// assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
||||
/// assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
||||
/// ```
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn format_bytes_speed(speed: u64) -> String {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
|
||||
+131
-12
@@ -1,16 +1,21 @@
|
||||
use crate::config::*;
|
||||
use crate::core::handle;
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{
|
||||
config::*,
|
||||
core::handle,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use chrono::{Local, TimeZone};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use log4rs::{
|
||||
append::{console::ConsoleAppender, file::FileAppender},
|
||||
config::{Appender, Logger, Root},
|
||||
encode::pattern::PatternEncoder,
|
||||
};
|
||||
use std::{
|
||||
fs::{self, DirEntry},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
/// initialize this instance's log file
|
||||
@@ -133,6 +138,118 @@ pub fn delete_log() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 初始化DNS配置文件
|
||||
fn init_dns_config() -> Result<()> {
|
||||
use serde_yaml::Value;
|
||||
|
||||
// 获取默认DNS配置
|
||||
let default_dns_config = serde_yaml::Mapping::from_iter([
|
||||
("enable".into(), Value::Bool(true)),
|
||||
("listen".into(), Value::String(":53".into())),
|
||||
("enhanced-mode".into(), Value::String("fake-ip".into())),
|
||||
(
|
||||
"fake-ip-range".into(),
|
||||
Value::String("198.18.0.1/16".into()),
|
||||
),
|
||||
(
|
||||
"fake-ip-filter-mode".into(),
|
||||
Value::String("blacklist".into()),
|
||||
),
|
||||
("prefer-h3".into(), Value::Bool(false)),
|
||||
("respect-rules".into(), Value::Bool(false)),
|
||||
("use-hosts".into(), Value::Bool(false)),
|
||||
("use-system-hosts".into(), Value::Bool(false)),
|
||||
(
|
||||
"fake-ip-filter".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("*.lan".into()),
|
||||
Value::String("*.local".into()),
|
||||
Value::String("*.arpa".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("ntp.*.com".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("+.market.xiaomi.com".into()),
|
||||
Value::String("localhost.ptlogin2.qq.com".into()),
|
||||
Value::String("*.msftncsi.com".into()),
|
||||
Value::String("www.msftconnecttest.com".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"default-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("223.6.6.6".into()),
|
||||
Value::String("8.8.8.8".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("8.8.8.8".into()),
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"fallback".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
Value::String("https://dns.google/dns-query".into()),
|
||||
Value::String("https://cloudflare-dns.com/dns-query".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"nameserver-policy".into(),
|
||||
Value::Mapping(serde_yaml::Mapping::new()),
|
||||
),
|
||||
(
|
||||
"proxy-server-nameserver".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("https://doh.pub/dns-query".into()),
|
||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||
]),
|
||||
),
|
||||
("direct-nameserver".into(), Value::Sequence(vec![])),
|
||||
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
|
||||
(
|
||||
"fallback-filter".into(),
|
||||
Value::Mapping(serde_yaml::Mapping::from_iter([
|
||||
("geoip".into(), Value::Bool(true)),
|
||||
("geoip-code".into(), Value::String("CN".into())),
|
||||
(
|
||||
"ipcidr".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("240.0.0.0/4".into()),
|
||||
Value::String("0.0.0.0/32".into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"domain".into(),
|
||||
Value::Sequence(vec![
|
||||
Value::String("+.google.com".into()),
|
||||
Value::String("+.facebook.com".into()),
|
||||
Value::String("+.youtube.com".into()),
|
||||
]),
|
||||
),
|
||||
])),
|
||||
),
|
||||
]);
|
||||
|
||||
// 检查DNS配置文件是否存在
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::info!(target: "app", "Creating default DNS config file");
|
||||
help::save_yaml(
|
||||
&dns_path,
|
||||
&default_dns_config,
|
||||
Some("# Clash Verge DNS Config"),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
@@ -173,6 +290,9 @@ pub fn init_config() -> Result<()> {
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
// 初始化DNS配置文件
|
||||
let _ = init_dns_config();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -245,8 +365,7 @@ pub fn init_resources() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn init_scheme() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
use winreg::{enums::*, RegKey};
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod dirs;
|
||||
pub mod error;
|
||||
pub mod help;
|
||||
pub mod i18n;
|
||||
pub mod init;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod tmpl;
|
||||
pub mod i18n;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::config::IVerge;
|
||||
use crate::utils::error;
|
||||
use crate::{config::Config, config::PrfItem, core::*, utils::init, utils::server};
|
||||
use crate::{log_err, wrap_err};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::{Config, IVerge, PrfItem}, core::*, log_err, module::lightweight, utils::{error, init, server}, wrap_err
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use percent_encoding::percent_decode_str;
|
||||
@@ -37,7 +38,10 @@ pub fn find_unused_port() -> Result<u16> {
|
||||
pub async fn resolve_setup(app: &mut App) {
|
||||
error::redirect_panic_to_log();
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
{
|
||||
AppHandleManager::global().init(app.app_handle().clone());
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
}
|
||||
let version = app.package_info().version.to_string();
|
||||
|
||||
handle::Handle::global().init(app.app_handle());
|
||||
@@ -72,14 +76,12 @@ pub async fn resolve_setup(app: &mut App) {
|
||||
}
|
||||
}
|
||||
if !service_runing {
|
||||
log::error!(target: "app", "service not runing. exit");
|
||||
app.app_handle().exit(-2);
|
||||
log::warn!(target: "app", "service not running, will fallback to user mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{e:?}");
|
||||
app.app_handle().exit(-1);
|
||||
log::warn!(target: "app", "failed to install service: {e:?}, will fallback to user mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +95,7 @@ pub async fn resolve_setup(app: &mut App) {
|
||||
|
||||
log::trace!(target: "app", "init system tray");
|
||||
log_err!(tray::Tray::global().init());
|
||||
log_err!(tray::Tray::global().create_systray());
|
||||
log_err!(tray::Tray::global().create_systray(app));
|
||||
|
||||
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
||||
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
||||
@@ -109,6 +111,11 @@ pub async fn resolve_setup(app: &mut App) {
|
||||
|
||||
log_err!(tray::Tray::global().update_part());
|
||||
log_err!(timer::Timer::global().init());
|
||||
|
||||
let enable_auto_light_weight_mode = { Config::verge().data().enable_auto_light_weight_mode };
|
||||
if enable_auto_light_weight_mode.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
}
|
||||
}
|
||||
|
||||
/// reset system proxy
|
||||
@@ -130,6 +137,8 @@ pub fn create_window() {
|
||||
log::info!(target: "app", "Starting to create window");
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window, trying to show it");
|
||||
@@ -196,7 +205,7 @@ pub fn create_window() {
|
||||
log::info!(target: "app", "Window created successfully, attempting to show");
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
|
||||
|
||||
// 设置窗口状态监控,实时保存窗口位置和大小
|
||||
crate::feat::setup_window_state_monitor(&app_handle);
|
||||
}
|
||||
@@ -310,8 +319,7 @@ fn resolve_random_port_config() -> Result<()> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn set_public_dns(dns_server: String) {
|
||||
use crate::core::handle;
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
|
||||
@@ -347,8 +355,7 @@ pub async fn set_public_dns(dns_server: String) {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn restore_public_dns() {
|
||||
use crate::core::handle;
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
log::info!(target: "app", "try to unset system dns");
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
extern crate warp;
|
||||
|
||||
use super::resolve;
|
||||
use crate::config::{Config, IVerge, DEFAULT_PAC};
|
||||
use crate::log_err;
|
||||
use crate::{
|
||||
config::{Config, IVerge, DEFAULT_PAC},
|
||||
log_err,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use std::convert::Infallible;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "mihomo_api"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
debug = []
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.12", features = ["json"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43.0", features = ["rt", "macros"] }
|
||||
@@ -0,0 +1,143 @@
|
||||
use reqwest::{Method, header::HeaderMap};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
pub mod model;
|
||||
pub use model::{MihomoData, MihomoManager};
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
|
||||
Self {
|
||||
mihomo_server,
|
||||
data: Arc::new(Mutex::new(MihomoData {
|
||||
proxies: serde_json::Value::Null,
|
||||
providers_proxies: serde_json::Value::Null,
|
||||
})),
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_proxies(&self, proxies: serde_json::Value) {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.proxies = proxies;
|
||||
}
|
||||
|
||||
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.providers_proxies = providers_proxies;
|
||||
}
|
||||
|
||||
pub fn get_mihomo_server(&self) -> String {
|
||||
self.mihomo_server.clone()
|
||||
}
|
||||
|
||||
pub fn get_proxies(&self) -> serde_json::Value {
|
||||
let data = self.data.lock().unwrap();
|
||||
data.proxies.clone()
|
||||
}
|
||||
|
||||
pub fn get_providers_proxies(&self) -> serde_json::Value {
|
||||
let data = self.data.lock().unwrap();
|
||||
data.providers_proxies.clone()
|
||||
}
|
||||
|
||||
async fn send_request(
|
||||
&self,
|
||||
method: Method,
|
||||
url: String,
|
||||
data: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let client_response = reqwest::ClientBuilder::new()
|
||||
.default_headers(self.headers.clone())
|
||||
.no_proxy()
|
||||
.timeout(Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?
|
||||
.request(method.clone(), &url)
|
||||
.json(&data.unwrap_or(json!({})))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let response = match method {
|
||||
Method::PATCH => {
|
||||
let status = client_response.status();
|
||||
if status.as_u16() == 204 {
|
||||
json!({"code": 204})
|
||||
} else {
|
||||
client_response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
}
|
||||
Method::PUT => json!(client_response.text().await.map_err(|e| e.to_string())?),
|
||||
_ => client_response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
|
||||
let url = format!("{}/proxies", self.mihomo_server);
|
||||
let proxies = self.send_request(Method::GET, url, None).await?;
|
||||
self.update_proxies(proxies);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
|
||||
let url = format!("{}/providers/proxies", self.mihomo_server);
|
||||
let providers_proxies = self.send_request(Method::GET, url, None).await?;
|
||||
self.update_providers_proxies(providers_proxies);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
pub async fn is_mihomo_running(&self) -> Result<(), String> {
|
||||
let url = format!("{}/version", self.mihomo_server);
|
||||
let _response = self.send_request(Method::GET, url, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
|
||||
let url = format!("{}/configs?force=true", self.mihomo_server);
|
||||
let payload = serde_json::json!({
|
||||
"path": clash_config_path,
|
||||
});
|
||||
let _response = self.send_request(Method::PUT, url, Some(payload)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> {
|
||||
let url = format!("{}/configs", self.mihomo_server);
|
||||
let response = self.send_request(Method::PATCH, url, Some(config)).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_proxy_delay(
|
||||
&self,
|
||||
name: &str,
|
||||
test_url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let test_url = test_url.unwrap_or("http://cp.cloudflare.com/generate_204".to_string());
|
||||
let url = format!(
|
||||
"{}/proxies/{}/delay?url={}&timeout={}",
|
||||
self.mihomo_server, name, test_url, timeout
|
||||
);
|
||||
let response = self.send_request(Method::GET, url, None).await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
pub struct MihomoData {
|
||||
pub(crate) proxies: serde_json::Value,
|
||||
pub(crate) providers_proxies: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MihomoManager {
|
||||
pub(crate) mihomo_server: String,
|
||||
pub(crate) data: Arc<Mutex<MihomoData>>,
|
||||
pub(crate) headers: HeaderMap,
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl Drop for MihomoData {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping MihomoData");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl Drop for MihomoManager {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping MihomoManager");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use mihomo_api;
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
#[test]
|
||||
fn test_mihomo_manager_init() {
|
||||
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
|
||||
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
|
||||
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_proxies() {
|
||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
||||
let manager = manager.refresh_proxies().await.unwrap();
|
||||
let proxies = manager.get_proxies();
|
||||
let providers = manager.get_providers_proxies();
|
||||
assert_ne!(proxies, serde_json::Value::Null);
|
||||
assert_eq!(providers, serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_providers_proxies() {
|
||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
||||
let manager = manager.refresh_providers_proxies().await.unwrap();
|
||||
let proxies = manager.get_proxies();
|
||||
let providers = manager.get_providers_proxies();
|
||||
assert_eq!(proxies, serde_json::Value::Null);
|
||||
assert_ne!(providers, serde_json::Value::Null);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"version": "2.2.1",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
@@ -16,7 +17,7 @@
|
||||
"copyright": "GNU General Public License v3.0",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Verge Rev",
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"build": {
|
||||
"beforeBuildCommand": "pnpm run web:build",
|
||||
@@ -25,24 +26,18 @@
|
||||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Verge",
|
||||
"version": "2.1.1",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"windows": {
|
||||
"installMode": "basicUi"
|
||||
}
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["clash", "clash-verge"]
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"endpoints": [
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/alpha/update-alpha-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/alpha/update-alpha.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
|
||||
@@ -30,10 +30,5 @@
|
||||
"./sidecar/verge-mihomo",
|
||||
"./sidecar/verge-mihomo-alpha"
|
||||
]
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"productName": "Clash Verge",
|
||||
"bundle": {
|
||||
"targets": ["app", "dmg"],
|
||||
"macOS": {
|
||||
@@ -29,11 +30,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon-mono.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true,
|
||||
"showMenuOnLeftClick": false
|
||||
},
|
||||
"windows": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,5 @@
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,5 @@
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,5 @@
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" rx="18" fill="url(#paint0_linear_3004_316)"/>
|
||||
<path d="M23 14.6666H22.1667V13C22.1667 10.7 20.3 8.83331 18 8.83331C15.7 8.83331 13.8334 10.7 13.8334 13H15.5C15.5 11.6166 16.6167 10.5 18 10.5C19.3834 10.5 20.5 11.6166 20.5 13V14.6666H13C12.0834 14.6666 11.3334 15.4166 11.3334 16.3333V24.6666C11.3334 25.5833 12.0834 26.3333 13 26.3333H23C23.9167 26.3333 24.6667 25.5833 24.6667 24.6666V16.3333C24.6667 15.4166 23.9167 14.6666 23 14.6666ZM23 24.6666H13V16.3333H23V24.6666ZM18 22.1666C18.9167 22.1666 19.6667 21.4166 19.6667 20.5C19.6667 19.5833 18.9167 18.8333 18 18.8333C17.0834 18.8333 16.3334 19.5833 16.3334 20.5C16.3334 21.4166 17.0834 22.1666 18 22.1666Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3004_316" x1="31" y1="27.5" x2="6.5" y2="7" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFA800"/>
|
||||
<stop offset="1" stop-color="#FFAC4B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1020 B |
@@ -111,7 +111,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0px;
|
||||
right: 1px;
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ interface NoticeInstance {
|
||||
|
||||
let parent: HTMLDivElement = null!;
|
||||
|
||||
// @ts-ignore
|
||||
export const Notice: NoticeInstance = (props) => {
|
||||
const { type, message, duration } = props;
|
||||
|
||||
@@ -142,8 +141,8 @@ export const Notice: NoticeInstance = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
(["info", "error", "success"] as const).forEach((type) => {
|
||||
Notice[type] = (message: ReactNode, duration?: number) => {
|
||||
const createNoticeTypeFactory =
|
||||
(type: keyof NoticeInstance) => (message: ReactNode, duration?: number) => {
|
||||
// 确保消息不为空
|
||||
if (!message) {
|
||||
return;
|
||||
@@ -156,4 +155,7 @@ export const Notice: NoticeInstance = (props) => {
|
||||
duration: type === "error" ? 8000 : duration || 1500,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
Notice.info = createNoticeTypeFactory("info");
|
||||
Notice.error = createNoticeTypeFactory("error");
|
||||
Notice.success = createNoticeTypeFactory("success");
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Box, BoxProps } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
interface CenterProps extends BoxProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Center: React.FC<CenterProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
height="100%"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { t } from "i18next";
|
||||
@@ -14,6 +14,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
const theme = useTheme();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
@@ -30,7 +31,15 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{ maxWidth: "520px" }}
|
||||
sx={{
|
||||
".MuiSnackbarContent-root": {
|
||||
maxWidth: "520px",
|
||||
maxHeight: "480px",
|
||||
overflowY: "auto",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||
@@ -48,11 +57,15 @@ interface InnerProps {
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const theme = useTheme();
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.remoteDestination}`
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
const Destination = metadata.destinationIP
|
||||
? metadata.destinationIP
|
||||
: metadata.remoteDestination;
|
||||
|
||||
const information = [
|
||||
{ label: t("Host"), value: host },
|
||||
@@ -66,7 +79,10 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
label: t("UL Speed"),
|
||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{ label: t("Chains"), value: chains },
|
||||
{
|
||||
label: t("Chains"),
|
||||
value: chains,
|
||||
},
|
||||
{ label: t("Rule"), value: rule },
|
||||
{
|
||||
label: t("Process"),
|
||||
@@ -79,7 +95,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
label: t("Source"),
|
||||
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
},
|
||||
{ label: t("Destination"), value: metadata.remoteDestination },
|
||||
{ label: t("Destination"), value: Destination },
|
||||
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
|
||||
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
@@ -87,10 +103,11 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>: <span>{each.value}</span>
|
||||
<b>{each.label}</b>
|
||||
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -138,6 +138,9 @@ export const ConnectionTable = (props: Props) => {
|
||||
const { metadata, rulePayload } = each;
|
||||
const chains = [...each.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||
const Destination = metadata.destinationIP
|
||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
return {
|
||||
id: each.id,
|
||||
host: metadata.host
|
||||
@@ -152,7 +155,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
remoteDestination: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||
remoteDestination: Destination,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
connectionData: each,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography, Stack, Divider } from "@mui/material";
|
||||
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import useSWR from "swr";
|
||||
import { getRules } from "@/services/api";
|
||||
import { getAppUptime, getSystemProxy } from "@/services/cmds";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
// 将毫秒转换为时:分:秒格式的函数
|
||||
const formatUptime = (uptimeMs: number) => {
|
||||
const hours = Math.floor(uptimeMs / 3600000);
|
||||
const minutes = Math.floor((uptimeMs % 3600000) / 60000);
|
||||
const seconds = Math.floor((uptimeMs % 60000) / 1000);
|
||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
export const ClashInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { version: clashVersion } = useClash();
|
||||
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
|
||||
const [rules, setRules] = useState<any[]>([]);
|
||||
|
||||
// 使用SWR获取应用运行时间,降低更新频率
|
||||
const { data: uptimeMs = 0 } = useSWR(
|
||||
"appUptime",
|
||||
getAppUptime,
|
||||
{
|
||||
refreshInterval: 1000,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 1000,
|
||||
},
|
||||
);
|
||||
|
||||
// 在组件加载时获取系统代理信息和规则数据
|
||||
useEffect(() => {
|
||||
// 获取系统代理信息
|
||||
getSystemProxy().then(setSysproxy);
|
||||
|
||||
// 获取规则数据
|
||||
getRules().then(setRules).catch(() => setRules([]));
|
||||
}, []);
|
||||
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
||||
|
||||
// 使用备忘录组件内容,减少重新渲染
|
||||
const cardContent = useMemo(() => {
|
||||
if (!clashInfo) return null;
|
||||
|
||||
return (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Core Version")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashVersion || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("System Proxy Address")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{sysproxy?.server || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Mixed Port")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.mixed_port || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Uptime")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{uptime}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Rules Count")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{rules.length}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Clash Info")}
|
||||
icon={<DeveloperBoardOutlined />}
|
||||
iconColor="warning"
|
||||
action={null}
|
||||
>
|
||||
{cardContent}
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import useSWR from "swr";
|
||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import {
|
||||
LanguageRounded,
|
||||
MultipleStopRounded,
|
||||
DirectionsRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const ClashModeCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
|
||||
// 获取当前Clash配置
|
||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
dedupingInterval: 1000,
|
||||
errorRetryInterval: 5000
|
||||
}
|
||||
);
|
||||
|
||||
// 支持的模式列表
|
||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||
|
||||
// 直接使用API返回的模式,不维护本地状态
|
||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
// 模式图标映射
|
||||
const modeIcons = useMemo(() => ({
|
||||
rule: <MultipleStopRounded fontSize="small" />,
|
||||
global: <LanguageRounded fontSize="small" />,
|
||||
direct: <DirectionsRounded fontSize="small" />
|
||||
}), []);
|
||||
|
||||
// 切换模式的处理函数
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
if (mode === currentMode) return;
|
||||
if (verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
|
||||
try {
|
||||
await patchClashMode(mode);
|
||||
mutateClash();
|
||||
} catch (error) {
|
||||
console.error("Failed to change mode:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 按钮样式
|
||||
const buttonStyles = (mode: string) => ({
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1.2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
|
||||
color: mode === currentMode ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
overflow: "visible",
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: 1,
|
||||
},
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after": mode === currentMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
// 描述样式
|
||||
const descriptionStyles = {
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
{/* 模式选择按钮组 */}
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
py: 1,
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{modeList.map((mode) => (
|
||||
<Paper
|
||||
key={mode}
|
||||
elevation={mode === currentMode ? 2 : 0}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={buttonStyles(mode)}
|
||||
>
|
||||
{modeIcons[mode]}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontWeight: mode === currentMode ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{t(mode)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* 说明文本区域 */}
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
my: 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={descriptionStyles}
|
||||
>
|
||||
{t(`${currentMode} Mode Description`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,645 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Button,
|
||||
alpha,
|
||||
useTheme,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
SelectChangeEvent,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
|
||||
import {
|
||||
SignalWifi4Bar as SignalStrong,
|
||||
SignalWifi3Bar as SignalGood,
|
||||
SignalWifi2Bar as SignalMedium,
|
||||
SignalWifi1Bar as SignalWeak,
|
||||
SignalWifi0Bar as SignalNone,
|
||||
WifiOff as SignalError,
|
||||
ChevronRight,
|
||||
} from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCurrentProxy } from "@/hooks/use-current-proxy";
|
||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||
import {
|
||||
getProxies,
|
||||
updateProxy,
|
||||
getConnections,
|
||||
deleteConnection,
|
||||
} from "@/services/api";
|
||||
import delayManager from "@/services/delay";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
|
||||
// 本地存储的键名
|
||||
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
||||
const STORAGE_KEY_PROXY = "clash-verge-selected-proxy";
|
||||
|
||||
// 代理节点信息接口
|
||||
interface ProxyOption {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 将delayManager返回的颜色格式转换为MUI Chip组件需要的格式
|
||||
function convertDelayColor(delayValue: number) {
|
||||
const colorStr = delayManager.formatDelayColor(delayValue);
|
||||
if (!colorStr) return "default";
|
||||
|
||||
// 从"error.main"这样的格式转为"error"
|
||||
const mainColor = colorStr.split(".")[0];
|
||||
|
||||
switch (mainColor) {
|
||||
case "success":
|
||||
return "success";
|
||||
case "warning":
|
||||
return "warning";
|
||||
case "error":
|
||||
return "error";
|
||||
case "primary":
|
||||
return "primary";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
// 根据延迟值获取合适的WiFi信号图标
|
||||
function getSignalIcon(delay: number) {
|
||||
if (delay < 0)
|
||||
return { icon: <SignalNone />, text: "未测试", color: "text.secondary" };
|
||||
if (delay >= 10000)
|
||||
return { icon: <SignalError />, text: "超时", color: "error.main" };
|
||||
if (delay >= 500)
|
||||
return { icon: <SignalWeak />, text: "延迟较高", color: "error.main" };
|
||||
if (delay >= 300)
|
||||
return { icon: <SignalMedium />, text: "延迟中等", color: "warning.main" };
|
||||
if (delay >= 200)
|
||||
return { icon: <SignalGood />, text: "延迟良好", color: "info.main" };
|
||||
return { icon: <SignalStrong />, text: "延迟极佳", color: "success.main" };
|
||||
}
|
||||
|
||||
// 简单的防抖函数
|
||||
function debounce(fn: Function, ms = 100) {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return function (this: any, ...args: any[]) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
export const CurrentProxyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
||||
useCurrentProxy();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { verge } = useVerge();
|
||||
|
||||
// 判断模式
|
||||
const isGlobalMode = mode === "global";
|
||||
const isDirectMode = mode === "direct";
|
||||
|
||||
// 使用 useRef 存储最后一次刷新时间和是否正在刷新
|
||||
const lastRefreshRef = useRef<number>(0);
|
||||
const isRefreshingRef = useRef<boolean>(false);
|
||||
const pendingRefreshRef = useRef<boolean>(false);
|
||||
|
||||
// 定义状态类型
|
||||
type ProxyState = {
|
||||
proxyData: {
|
||||
groups: { name: string; now: string; all: string[] }[];
|
||||
records: Record<string, any>;
|
||||
globalProxy: string;
|
||||
directProxy: any;
|
||||
};
|
||||
selection: {
|
||||
group: string;
|
||||
proxy: string;
|
||||
};
|
||||
displayProxy: any;
|
||||
};
|
||||
|
||||
// 合并状态,减少状态更新次数
|
||||
const [state, setState] = useState<ProxyState>({
|
||||
proxyData: {
|
||||
groups: [],
|
||||
records: {},
|
||||
globalProxy: "",
|
||||
directProxy: null,
|
||||
},
|
||||
selection: {
|
||||
group: "",
|
||||
proxy: "",
|
||||
},
|
||||
displayProxy: null,
|
||||
});
|
||||
|
||||
// 初始化选择的组
|
||||
useEffect(() => {
|
||||
// 根据模式确定初始组
|
||||
if (isGlobalMode) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: "GLOBAL",
|
||||
},
|
||||
}));
|
||||
} else if (isDirectMode) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: "DIRECT",
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: savedGroup || primaryGroupName || "",
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
||||
|
||||
// 带锁的代理数据获取函数,防止并发请求
|
||||
const fetchProxyData = useCallback(
|
||||
async (force = false) => {
|
||||
// 防止重复请求
|
||||
if (isRefreshingRef.current) {
|
||||
pendingRefreshRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查刷新间隔
|
||||
const now = Date.now();
|
||||
if (!force && now - lastRefreshRef.current < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRefreshingRef.current = true;
|
||||
lastRefreshRef.current = now;
|
||||
|
||||
try {
|
||||
const data = await getProxies();
|
||||
|
||||
// 过滤和格式化组
|
||||
const filteredGroups = data.groups
|
||||
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||
.map((g) => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p) => p.name),
|
||||
}));
|
||||
|
||||
// 使用函数式更新确保状态更新的原子性
|
||||
setState((prev) => {
|
||||
let newProxy = "";
|
||||
let newDisplayProxy = null;
|
||||
let newGroup = prev.selection.group;
|
||||
|
||||
// 根据模式确定新代理
|
||||
if (isDirectMode) {
|
||||
newGroup = "DIRECT";
|
||||
newProxy = "DIRECT";
|
||||
newDisplayProxy = data.records?.DIRECT || null;
|
||||
} else if (isGlobalMode && data.global) {
|
||||
newGroup = "GLOBAL";
|
||||
newProxy = data.global.now || "";
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
} else {
|
||||
// 普通模式 - 检查当前选择的组是否存在
|
||||
const currentGroup = filteredGroups.find(
|
||||
(g) => g.name === prev.selection.group,
|
||||
);
|
||||
|
||||
// 如果当前组不存在或为空,自动选择第一个组
|
||||
if (!currentGroup && filteredGroups.length > 0) {
|
||||
newGroup = filteredGroups[0].name;
|
||||
const firstGroup = filteredGroups[0];
|
||||
newProxy = firstGroup.now;
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
|
||||
// 保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
}
|
||||
} else if (currentGroup) {
|
||||
// 使用当前组的代理
|
||||
newProxy = currentGroup.now;
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回新状态
|
||||
return {
|
||||
proxyData: {
|
||||
groups: filteredGroups,
|
||||
records: data.records || {},
|
||||
globalProxy: data.global?.now || "",
|
||||
directProxy: data.records?.DIRECT || null,
|
||||
},
|
||||
selection: {
|
||||
group: newGroup,
|
||||
proxy: newProxy,
|
||||
},
|
||||
displayProxy: newDisplayProxy,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取代理信息失败", error);
|
||||
} finally {
|
||||
isRefreshingRef.current = false;
|
||||
|
||||
// 处理待处理的刷新请求
|
||||
if (pendingRefreshRef.current) {
|
||||
pendingRefreshRef.current = false;
|
||||
setTimeout(() => fetchProxyData(), 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isGlobalMode, isDirectMode],
|
||||
);
|
||||
|
||||
// 响应 currentProxy 变化
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentProxy &&
|
||||
(!state.displayProxy || currentProxy.name !== state.displayProxy.name)
|
||||
) {
|
||||
fetchProxyData(true);
|
||||
}
|
||||
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
||||
|
||||
// 监听模式变化,mode变化时刷新
|
||||
useEffect(() => {
|
||||
fetchProxyData(true);
|
||||
}, [mode, fetchProxyData]);
|
||||
|
||||
|
||||
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||
const proxyOptions = useMemo(() => {
|
||||
if (isDirectMode) {
|
||||
return [{ name: "DIRECT" }];
|
||||
}
|
||||
if (isGlobalMode && state.proxyData.records) {
|
||||
// 全局模式下的选项
|
||||
return Object.keys(state.proxyData.records)
|
||||
.filter((name) => name !== "DIRECT" && name !== "REJECT")
|
||||
.map((name) => ({ name }));
|
||||
}
|
||||
|
||||
// 普通模式
|
||||
const group = state.proxyData.groups.find(
|
||||
(g) => g.name === state.selection.group,
|
||||
);
|
||||
if (group) {
|
||||
return group.all.map((name) => ({ name }));
|
||||
}
|
||||
return [];
|
||||
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
||||
|
||||
// 使用防抖包装状态更新,避免快速连续更新
|
||||
const debouncedSetState = useCallback(
|
||||
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
||||
setState(updateFn);
|
||||
}, 50),
|
||||
[],
|
||||
);
|
||||
|
||||
// 处理代理组变更
|
||||
const handleGroupChange = useCallback(
|
||||
(event: SelectChangeEvent) => {
|
||||
if (isGlobalMode || isDirectMode) return;
|
||||
|
||||
const newGroup = event.target.value;
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
|
||||
// 获取该组当前选中的代理
|
||||
setState((prev) => {
|
||||
const group = prev.proxyData.groups.find((g) => g.name === newGroup);
|
||||
if (group) {
|
||||
return {
|
||||
...prev,
|
||||
selection: {
|
||||
group: newGroup,
|
||||
proxy: group.now,
|
||||
},
|
||||
displayProxy: prev.proxyData.records[group.now] || null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: newGroup,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[isGlobalMode, isDirectMode],
|
||||
);
|
||||
|
||||
// 处理代理节点变更
|
||||
const handleProxyChange = useCallback(
|
||||
async (event: SelectChangeEvent) => {
|
||||
if (isDirectMode) return;
|
||||
|
||||
const newProxy = event.target.value;
|
||||
const currentGroup = state.selection.group;
|
||||
const previousProxy = state.selection.proxy;
|
||||
|
||||
// 立即更新UI,优化体验
|
||||
debouncedSetState((prev: ProxyState) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
proxy: newProxy,
|
||||
},
|
||||
displayProxy: prev.proxyData.records[newProxy] || null,
|
||||
}));
|
||||
|
||||
// 非特殊模式下保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新代理设置
|
||||
await updateProxy(currentGroup, newProxy);
|
||||
|
||||
// 自动关闭连接设置
|
||||
if (verge?.auto_close_connection && previousProxy) {
|
||||
getConnections().then(({ connections }) => {
|
||||
connections.forEach((conn) => {
|
||||
if (conn.chains.includes(previousProxy)) {
|
||||
deleteConnection(conn.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新代理信息,使用较短的延迟
|
||||
setTimeout(() => {
|
||||
refreshProxy();
|
||||
fetchProxyData(true);
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error("更新代理失败", error);
|
||||
}
|
||||
},
|
||||
[
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
state.proxyData.records,
|
||||
state.selection,
|
||||
verge?.auto_close_connection,
|
||||
refreshProxy,
|
||||
fetchProxyData,
|
||||
debouncedSetState,
|
||||
],
|
||||
);
|
||||
|
||||
// 导航到代理页面
|
||||
const goToProxies = useCallback(() => {
|
||||
navigate("/");
|
||||
}, [navigate]);
|
||||
|
||||
// 获取要显示的代理节点
|
||||
const proxyToDisplay = state.displayProxy || currentProxy;
|
||||
|
||||
// 获取当前节点的延迟
|
||||
const currentDelay = proxyToDisplay
|
||||
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
||||
: -1;
|
||||
|
||||
// 获取信号图标
|
||||
const signalInfo = getSignalIcon(currentDelay);
|
||||
|
||||
// 自定义渲染选择框中的值
|
||||
const renderProxyValue = useCallback(
|
||||
(selected: string) => {
|
||||
if (!selected || !state.proxyData.records[selected]) return selected;
|
||||
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[selected],
|
||||
state.selection.group,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography noWrap>{selected}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delayValue)}
|
||||
color={convertDelayColor(delayValue)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
[state.proxyData.records, state.selection.group],
|
||||
);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Current Node")}
|
||||
icon={
|
||||
<Tooltip
|
||||
title={
|
||||
proxyToDisplay
|
||||
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
||||
: "无代理节点"
|
||||
}
|
||||
>
|
||||
<Box sx={{ color: signalInfo.color }}>
|
||||
{proxyToDisplay ? signalInfo.icon : <SignalNone color="disabled" />}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
iconColor={proxyToDisplay ? "primary" : undefined}
|
||||
action={
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={goToProxies}
|
||||
sx={{ borderRadius: 1.5 }}
|
||||
endIcon={<ChevronRight fontSize="small" />}
|
||||
>
|
||||
{t("Label-Proxies")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{proxyToDisplay ? (
|
||||
<Box>
|
||||
{/* 代理节点信息显示 */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
p: 1,
|
||||
mb: 2,
|
||||
borderRadius: 1,
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.05),
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body1" fontWeight="medium">
|
||||
{proxyToDisplay.name}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{proxyToDisplay.type}
|
||||
</Typography>
|
||||
{isGlobalMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Global Mode")}
|
||||
color="primary"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
{isDirectMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Direct Mode")}
|
||||
color="success"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
{/* 节点特性 */}
|
||||
{proxyToDisplay.udp && (
|
||||
<Chip size="small" label="UDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.tfo && (
|
||||
<Chip size="small" label="TFO" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.xudp && (
|
||||
<Chip size="small" label="XUDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.mptcp && (
|
||||
<Chip size="small" label="MPTCP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.smux && (
|
||||
<Chip size="small" label="SMUX" variant="outlined" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 显示延迟 */}
|
||||
{proxyToDisplay && !isDirectMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(currentDelay)}
|
||||
color={convertDelayColor(currentDelay)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* 代理组选择器 */}
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mb: 1.5 }}
|
||||
>
|
||||
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
|
||||
<Select
|
||||
labelId="proxy-group-select-label"
|
||||
value={state.selection.group}
|
||||
onChange={handleGroupChange}
|
||||
label={t("Group")}
|
||||
disabled={isGlobalMode || isDirectMode}
|
||||
>
|
||||
{state.proxyData.groups.map((group) => (
|
||||
<MenuItem key={group.name} value={group.name}>
|
||||
{group.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 代理节点选择器 */}
|
||||
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 0 }}>
|
||||
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
|
||||
<Select
|
||||
labelId="proxy-select-label"
|
||||
value={state.selection.proxy}
|
||||
onChange={handleProxyChange}
|
||||
label={t("Proxy")}
|
||||
disabled={isDirectMode}
|
||||
renderValue={renderProxyValue}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 500,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isDirectMode
|
||||
? null
|
||||
: proxyOptions.map((proxy) => {
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group,
|
||||
);
|
||||
return (
|
||||
<MenuItem
|
||||
key={proxy.name}
|
||||
value={proxy.name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{proxy.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delayValue)}
|
||||
color={convertDelayColor(delayValue)}
|
||||
sx={{
|
||||
minWidth: "60px",
|
||||
height: "22px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ textAlign: "center", py: 4 }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t("No active proxy node")}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user