mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-20 09:02:07 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceea4f72f8 | ||
|
|
6aca27ea97 | ||
|
|
71ae357be8 | ||
|
|
2bbcdb1d95 | ||
|
|
646df28d1c | ||
|
|
f5f2ee1f95 | ||
|
|
0842c7343d | ||
|
|
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 |
@@ -12,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)
|
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下进行讨论
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||||
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
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:
|
## 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)
|
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
|
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
|
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
|
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 describe the problem in detail according to the template specification, otherwise the issue will be closed
|
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
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
|||||||
+149
-107
@@ -3,8 +3,8 @@ name: Alpha Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# UTC+8 00:00 (UTC 16:00 previous day) and UTC+8 12:00 (UTC 04:00)
|
# UTC+8 0,6,12,18
|
||||||
- cron: "0 16,4 * * *"
|
- cron: "0 16,22,4,10 * * *"
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -37,15 +37,15 @@ jobs:
|
|||||||
# Store current version from package.json
|
# Store current version from package.json
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
echo "Current version: $CURRENT_VERSION"
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
# Get the previous commit's package.json version
|
# Get the previous commit's package.json version
|
||||||
git checkout HEAD~1 package.json
|
git checkout HEAD~1 package.json
|
||||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||||
echo "Previous version: $PREVIOUS_VERSION"
|
echo "Previous version: $PREVIOUS_VERSION"
|
||||||
|
|
||||||
# Reset back to current commit
|
# Reset back to current commit
|
||||||
git checkout HEAD package.json
|
git checkout HEAD package.json
|
||||||
|
|
||||||
# Check if version changed
|
# Check if version changed
|
||||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||||
@@ -54,24 +54,145 @@ jobs:
|
|||||||
echo "Version unchanged: $CURRENT_VERSION"
|
echo "Version unchanged: $CURRENT_VERSION"
|
||||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
delete_old_release:
|
delete_old_assets:
|
||||||
needs: check_commit
|
needs: check_commit
|
||||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Delete Old Alpha Release
|
- name: Delete Old Alpha Release Assets
|
||||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
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:
|
with:
|
||||||
tag_name: alpha
|
tag_name: alpha
|
||||||
delete_release: true
|
name: "Clash Verge Rev Alpha"
|
||||||
repo: ${{ github.repository }}
|
body_path: release.txt
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
prerelease: true
|
||||||
env:
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
generate_release_notes: true
|
||||||
|
|
||||||
alpha:
|
alpha:
|
||||||
needs: delete_old_release
|
needs: update_tag
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -109,7 +230,7 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -126,6 +247,9 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release Alpha Version
|
||||||
|
run: pnpm release-alpha-version
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -149,7 +273,7 @@ jobs:
|
|||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
alpha-for-linux-arm:
|
alpha-for-linux-arm:
|
||||||
needs: delete_old_release
|
needs: update_tag
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -192,6 +316,9 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release Alpha Version
|
||||||
|
run: pnpm release-alpha-version
|
||||||
|
|
||||||
- name: "Setup for linux"
|
- name: "Setup for linux"
|
||||||
run: |-
|
run: |-
|
||||||
sudo ls -lR /etc/apt/
|
sudo ls -lR /etc/apt/
|
||||||
@@ -215,6 +342,7 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
|
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
@@ -263,7 +391,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: alpha
|
tag_name: alpha
|
||||||
name: "Clash Verge Rev Alpha"
|
name: "Clash Verge Rev Alpha"
|
||||||
body: "More new features are now supported."
|
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
@@ -271,7 +398,7 @@ jobs:
|
|||||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
alpha-for-fixed-webview2:
|
alpha-for-fixed-webview2:
|
||||||
needs: delete_old_release
|
needs: update_tag
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -312,6 +439,9 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release Alpha Version
|
||||||
|
run: pnpm release-alpha-version
|
||||||
|
|
||||||
- name: Download WebView2 Runtime
|
- name: Download WebView2 Runtime
|
||||||
run: |
|
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
|
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
|
||||||
@@ -356,7 +486,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: alpha
|
tag_name: alpha
|
||||||
name: "Clash Verge Rev Alpha"
|
name: "Clash Verge Rev Alpha"
|
||||||
body: "More new features are now supported."
|
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
@@ -365,90 +494,3 @@ jobs:
|
|||||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
update_tag:
|
|
||||||
name: Update tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [delete_old_release, alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -153,6 +153,7 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
|
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
|
|||||||
+66
-21
@@ -1,17 +1,60 @@
|
|||||||
## v2.2.1-alpha
|
## v2.2.2
|
||||||
**发行代号:拓**
|
|
||||||
|
|
||||||
#### 修复
|
|
||||||
1. **系统**
|
|
||||||
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
|
|
||||||
|
|
||||||
|
|
||||||
## v2.2.0
|
|
||||||
|
|
||||||
**发行代号:拓**
|
**发行代号:拓**
|
||||||
|
|
||||||
|
感谢 Tunglies 对 Verge 后端重构,性能优化做出的重大贡献!
|
||||||
|
|
||||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||||
|
|
||||||
|
#### 已知问题
|
||||||
|
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||||
|
|
||||||
|
### 2.2.2 相对于 2.2.1(已下架不在提供)
|
||||||
|
#### 修复了:
|
||||||
|
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||||
|
- MacOS进入轻量模式以后影藏Dock图标
|
||||||
|
- 增加轻量模式缺失的tray翻译
|
||||||
|
- Linux下的窗口边框被削掉的问题
|
||||||
|
|
||||||
|
#### 新增了:
|
||||||
|
- 加强服务检测和重装逻辑
|
||||||
|
- 增强内核与服务保活机制
|
||||||
|
- 增加服务模式下的僵尸进程清理机制
|
||||||
|
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||||
|
|
||||||
|
### 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 启用托盘速率显示时,可选隐藏托盘图标显示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2.0(已下架不在提供)
|
||||||
|
|
||||||
#### 新增功能
|
#### 新增功能
|
||||||
1. **首页**
|
1. **首页**
|
||||||
- 新增首页功能,默认启动页面改为首页。
|
- 新增首页功能,默认启动页面改为首页。
|
||||||
@@ -21,8 +64,8 @@
|
|||||||
- 限制首页配置文件卡片URL长度。
|
- 限制首页配置文件卡片URL长度。
|
||||||
|
|
||||||
2. **DNS 设置与覆写**
|
2. **DNS 设置与覆写**
|
||||||
- 默认启用 DNS 设置。
|
|
||||||
- 新增 DNS 覆写功能。
|
- 新增 DNS 覆写功能。
|
||||||
|
- 默认启用 DNS 覆写。
|
||||||
|
|
||||||
3. **解锁测试**
|
3. **解锁测试**
|
||||||
- 新增解锁测试页面。
|
- 新增解锁测试页面。
|
||||||
@@ -32,10 +75,11 @@
|
|||||||
- 添加自动轻量模式定时器。
|
- 添加自动轻量模式定时器。
|
||||||
|
|
||||||
5. **系统支持**
|
5. **系统支持**
|
||||||
|
- Mihomo(meta)内核升级 1.19.3
|
||||||
- macOS 支持 CMD+W 关闭窗口。
|
- macOS 支持 CMD+W 关闭窗口。
|
||||||
- 新增 macOS 应用菜单。
|
- 新增 macOS 应用菜单。
|
||||||
- 添加管理员权限提示。
|
- 添加 macOS 安装服务时候的管理员权限提示。
|
||||||
- 新增 sidecar 模式。
|
- 新增 sidecar(用户空间启动内核) 模式。
|
||||||
|
|
||||||
6. **其他**
|
6. **其他**
|
||||||
- 增强延迟测试日志和错误处理。
|
- 增强延迟测试日志和错误处理。
|
||||||
@@ -47,28 +91,29 @@
|
|||||||
- 修复 Windows 热键崩溃。
|
- 修复 Windows 热键崩溃。
|
||||||
- 修复 macOS 无框标题。
|
- 修复 macOS 无框标题。
|
||||||
- 修复 macOS 静默启动崩溃。
|
- 修复 macOS 静默启动崩溃。
|
||||||
|
- 修复 macOS tray图标错位到左上角的问题。
|
||||||
- 修复 Windows/Linux 运行时崩溃。
|
- 修复 Windows/Linux 运行时崩溃。
|
||||||
- 修复 Netflix 检测错误。
|
- 修复 Win10 阴影和边框问题。
|
||||||
- 修复服务模式检测失败。
|
- 修复 升级或重装后开机自启状态检测和同步问题。
|
||||||
|
|
||||||
2. **性能**
|
2. **构建**
|
||||||
- 优化小数值速度更新。
|
|
||||||
- 增加请求超时至 60 秒。
|
|
||||||
- 修复代理节点选择同步。
|
|
||||||
- 优化修改verge配置性能。
|
|
||||||
|
|
||||||
3. **构建**
|
|
||||||
- 修复构建失败问题。
|
- 修复构建失败问题。
|
||||||
|
|
||||||
#### 优化
|
#### 优化
|
||||||
1. **性能**
|
1. **性能**
|
||||||
|
- 重构后端,巨幅性能优化。
|
||||||
- 优化首页组件性能。
|
- 优化首页组件性能。
|
||||||
- 优化流量图表资源使用。
|
- 优化流量图表资源使用。
|
||||||
- 提升代理组列表滚动性能。
|
- 提升代理组列表滚动性能。
|
||||||
- 加快应用退出速度。
|
- 加快应用退出速度。
|
||||||
- 加快进入轻量模式速度。
|
- 加快进入轻量模式速度。
|
||||||
|
- 优化小数值速度更新。
|
||||||
|
- 增加请求超时至 60 秒。
|
||||||
|
- 修复代理节点选择同步。
|
||||||
|
- 优化修改verge配置性能。
|
||||||
|
|
||||||
2. **重构**
|
2. **重构**
|
||||||
|
- 重构后端,巨幅性能优化。
|
||||||
- 优化定时器管理。
|
- 优化定时器管理。
|
||||||
- 重构 MihomoManager 处理流量。
|
- 重构 MihomoManager 处理流量。
|
||||||
- 优化 WebSocket 连接。
|
- 优化 WebSocket 连接。
|
||||||
|
|||||||
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 |
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.2.1-alpha",
|
"version": "--help",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||||
"portable": "node scripts/portable.mjs",
|
"portable": "node scripts/portable.mjs",
|
||||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||||
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||||
|
"release-version": "node scripts/release_version.mjs",
|
||||||
|
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"clean": "cd ./src-tauri && cargo clean && cd -"
|
"clean": "cd ./src-tauri && cargo clean && cd -"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
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();
|
||||||
|
}
|
||||||
|
|||||||
+222
-211
@@ -1,6 +1,6 @@
|
|||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = "updater";
|
||||||
@@ -8,8 +8,8 @@ const UPDATE_JSON_FILE = "update.json";
|
|||||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
// Add alpha update JSON filenames
|
// Add alpha update JSON filenames
|
||||||
const ALPHA_TAG_NAME = "updater-alpha";
|
const ALPHA_TAG_NAME = "updater-alpha";
|
||||||
const ALPHA_UPDATE_JSON_FILE = "update-alpha.json";
|
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||||
const ALPHA_UPDATE_JSON_PROXY = "update-alpha-proxy.json";
|
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
@@ -78,224 +78,235 @@ async function resolveUpdater() {
|
|||||||
async function processRelease(github, options, tag, isAlpha) {
|
async function processRelease(github, options, tag, isAlpha) {
|
||||||
if (!tag) return;
|
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).catch(
|
|
||||||
() => "No changelog available",
|
|
||||||
),
|
|
||||||
pub_date: new Date().toISOString(),
|
|
||||||
platforms: {
|
|
||||||
win64: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
linux: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
"darwin-aarch64": { signature: "", url: "" },
|
|
||||||
"darwin-intel": { signature: "", url: "" },
|
|
||||||
"darwin-x86_64": { signature: "", url: "" },
|
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
|
||||||
"linux-x86": { signature: "", url: "" },
|
|
||||||
"linux-i686": { signature: "", url: "" },
|
|
||||||
"linux-aarch64": { signature: "", url: "" },
|
|
||||||
"linux-armv7": { signature: "", url: "" },
|
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
|
||||||
"windows-aarch64": { signature: "", url: "" },
|
|
||||||
"windows-x86": { signature: "", url: "" },
|
|
||||||
"windows-i686": { signature: "", url: "" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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.exe")) {
|
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win64 signature
|
|
||||||
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.exe")) {
|
|
||||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
|
||||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win32 signature
|
|
||||||
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.exe")) {
|
|
||||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win arm signature
|
|
||||||
if (name.endsWith("arm64-setup.exe.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["windows-aarch64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// darwin url (intel)
|
|
||||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
|
||||||
updateData.platforms.darwin.url = browser_download_url;
|
|
||||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
|
||||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// darwin signature (intel)
|
|
||||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms.darwin.signature = sig;
|
|
||||||
updateData.platforms["darwin-intel"].signature = sig;
|
|
||||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// darwin url (aarch)
|
|
||||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
|
||||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
|
||||||
// 使linux可以检查更新
|
|
||||||
updateData.platforms.linux.url = browser_download_url;
|
|
||||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-armv7"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// darwin signature (aarch)
|
|
||||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
|
||||||
updateData.platforms.linux.signature = sig;
|
|
||||||
updateData.platforms["linux-x86_64"].signature = sig;
|
|
||||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-aarch64"].signature = sig;
|
|
||||||
updateData.platforms["linux-armv7"].signature = sig;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
|
||||||
console.log(updateData);
|
|
||||||
|
|
||||||
// maybe should test the signature as well
|
|
||||||
// delete the null field
|
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
|
||||||
if (!value.url) {
|
|
||||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
|
||||||
delete updateData.platforms[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate a proxy update file for accelerated GitHub resources
|
|
||||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
|
||||||
|
|
||||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
|
||||||
if (value.url) {
|
|
||||||
updateDataNew.platforms[key].url =
|
|
||||||
"https://download.clashverge.dev/" + value.url;
|
|
||||||
} else {
|
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let updateRelease;
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: tag.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: tag.name,
|
||||||
|
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||||
|
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||||
|
),
|
||||||
|
pub_date: new Date().toISOString(),
|
||||||
|
platforms: {
|
||||||
|
win64: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
linux: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
"darwin-aarch64": { signature: "", url: "" },
|
||||||
|
"darwin-intel": { signature: "", url: "" },
|
||||||
|
"darwin-x86_64": { signature: "", url: "" },
|
||||||
|
"linux-x86_64": { signature: "", url: "" },
|
||||||
|
"linux-x86": { signature: "", url: "" },
|
||||||
|
"linux-i686": { signature: "", url: "" },
|
||||||
|
"linux-aarch64": { signature: "", url: "" },
|
||||||
|
"linux-armv7": { signature: "", url: "" },
|
||||||
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
|
"windows-x86": { signature: "", url: "" },
|
||||||
|
"windows-i686": { signature: "", url: "" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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.exe")) {
|
||||||
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win64 signature
|
||||||
|
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.exe")) {
|
||||||
|
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win32 signature
|
||||||
|
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.exe")) {
|
||||||
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win arm signature
|
||||||
|
if (name.endsWith("arm64-setup.exe.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-aarch64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// darwin url (intel)
|
||||||
|
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||||
|
updateData.platforms.darwin.url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// darwin signature (intel)
|
||||||
|
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.darwin.signature = sig;
|
||||||
|
updateData.platforms["darwin-intel"].signature = sig;
|
||||||
|
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// darwin url (aarch)
|
||||||
|
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||||
|
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||||
|
// 使linux可以检查更新
|
||||||
|
updateData.platforms.linux.url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-armv7"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// darwin signature (aarch)
|
||||||
|
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||||
|
updateData.platforms.linux.signature = sig;
|
||||||
|
updateData.platforms["linux-x86_64"].signature = sig;
|
||||||
|
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-aarch64"].signature = sig;
|
||||||
|
updateData.platforms["linux-armv7"].signature = sig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
console.log(updateData);
|
||||||
|
|
||||||
|
// maybe should test the signature as well
|
||||||
|
// delete the null field
|
||||||
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
|
if (!value.url) {
|
||||||
|
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||||
|
delete updateData.platforms[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a proxy update file for accelerated GitHub resources
|
||||||
|
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||||
|
|
||||||
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
|
if (value.url) {
|
||||||
|
updateDataNew.platforms[key].url =
|
||||||
|
"https://download.clashverge.dev/" + value.url;
|
||||||
|
} else {
|
||||||
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get the existing release
|
let updateRelease;
|
||||||
const response = await github.rest.repos.getReleaseByTag({
|
|
||||||
|
try {
|
||||||
|
// Try to get the existing release
|
||||||
|
const response = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
...options,
|
||||||
tag: releaseTag,
|
release_id: updateRelease.id,
|
||||||
|
name: jsonFile,
|
||||||
|
data: JSON.stringify(updateData, null, 2),
|
||||||
});
|
});
|
||||||
updateRelease = response.data;
|
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
...options,
|
||||||
|
release_id: updateRelease.id,
|
||||||
|
name: proxyFile,
|
||||||
|
data: JSON.stringify(updateDataNew, null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If release doesn't exist, create it
|
console.error(
|
||||||
if (error.status === 404) {
|
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
console.log(
|
error.message,
|
||||||
`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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
} catch (error) {
|
||||||
console.error(
|
if (error.status === 404) {
|
||||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||||
error.message,
|
} else {
|
||||||
);
|
console.error(
|
||||||
|
`Failed to get release for tag: ${tag.name}`,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+12
-1
@@ -1132,7 +1132,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.2.1-alpha"
|
version = "2.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
@@ -1146,6 +1146,7 @@ dependencies = [
|
|||||||
"dirs 6.0.0",
|
"dirs 6.0.0",
|
||||||
"dunce",
|
"dunce",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fs2",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
"image",
|
"image",
|
||||||
@@ -2339,6 +2340,16 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs2"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futf"
|
name = "futf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.2.1-alpha"
|
version = "2.2.2"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -34,6 +34,7 @@ port_scanner = "0.1.5"
|
|||||||
delay_timer = "0.11.6"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
|
fs2 = "0.4.3"
|
||||||
window-shadows = { version = "0.2.2" }
|
window-shadows = { version = "0.2.2" }
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -6,4 +6,4 @@ use super::CmdResult;
|
|||||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||||
lightweight::entry_lightweight_mode();
|
lightweight::entry_lightweight_mode();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub type CmdResult<T = ()> = Result<T, String>;
|
|||||||
// Command modules
|
// Command modules
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod clash;
|
pub mod clash;
|
||||||
|
pub mod core;
|
||||||
pub mod media_unlock_checker;
|
pub mod media_unlock_checker;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
@@ -22,6 +23,7 @@ pub mod lighteweight;
|
|||||||
// Re-export all command functions for backwards compatibility
|
// Re-export all command functions for backwards compatibility
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use clash::*;
|
pub use clash::*;
|
||||||
|
pub use core::*;
|
||||||
pub use media_unlock_checker::*;
|
pub use media_unlock_checker::*;
|
||||||
pub use network::*;
|
pub use network::*;
|
||||||
pub use profile::*;
|
pub use profile::*;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub async fn get_system_info() -> CmdResult<String> {
|
|||||||
pub async fn get_running_mode() -> Result<String, String> {
|
pub async fn get_running_mode() -> Result<String, String> {
|
||||||
match CoreManager::global().get_running_mode().await {
|
match CoreManager::global().get_running_mode().await {
|
||||||
core::RunningMode::Service => Ok("service".to_string()),
|
core::RunningMode::Service => Ok("service".to_string()),
|
||||||
core::RunningMode::Sidecar => Ok("sidecar".to_string()),
|
core::RunningMode::Sidecar => Ok("standalone".to_string()),
|
||||||
core::RunningMode::NotRunning => Ok("not_running".to_string()),
|
core::RunningMode::NotRunning => Ok("not_running".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,11 +189,16 @@ pub struct IVerge {
|
|||||||
|
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
|
||||||
|
pub enable_tray_icon: Option<bool>,
|
||||||
|
|
||||||
/// 自动进入轻量模式
|
/// 自动进入轻量模式
|
||||||
pub enable_auto_light_weight_mode: Option<bool>,
|
pub enable_auto_light_weight_mode: Option<bool>,
|
||||||
|
|
||||||
/// 自动进入轻量模式的延迟(分钟)
|
/// 自动进入轻量模式的延迟(分钟)
|
||||||
pub auto_light_weight_minutes: Option<u64>,
|
pub auto_light_weight_minutes: Option<u64>,
|
||||||
|
|
||||||
|
/// 服务状态跟踪
|
||||||
|
pub service_state: Option<crate::core::service::ServiceState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -295,11 +300,13 @@ impl IVerge {
|
|||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
enable_tray_speed: Some(true),
|
enable_tray_speed: Some(true),
|
||||||
|
enable_tray_icon: Some(true),
|
||||||
enable_global_hotkey: Some(true),
|
enable_global_hotkey: Some(true),
|
||||||
enable_auto_light_weight_mode: Some(false),
|
enable_auto_light_weight_mode: Some(false),
|
||||||
auto_light_weight_minutes: Some(10),
|
auto_light_weight_minutes: Some(10),
|
||||||
enable_dns_settings: Some(true),
|
enable_dns_settings: Some(true),
|
||||||
home_cards: None,
|
home_cards: None,
|
||||||
|
service_state: None,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,10 +388,12 @@ impl IVerge {
|
|||||||
patch!(webdav_username);
|
patch!(webdav_username);
|
||||||
patch!(webdav_password);
|
patch!(webdav_password);
|
||||||
patch!(enable_tray_speed);
|
patch!(enable_tray_speed);
|
||||||
|
patch!(enable_tray_icon);
|
||||||
patch!(enable_auto_light_weight_mode);
|
patch!(enable_auto_light_weight_mode);
|
||||||
patch!(auto_light_weight_minutes);
|
patch!(auto_light_weight_minutes);
|
||||||
patch!(enable_dns_settings);
|
patch!(enable_dns_settings);
|
||||||
patch!(home_cards);
|
patch!(home_cards);
|
||||||
|
patch!(service_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
@@ -473,10 +482,12 @@ pub struct IVergeResponse {
|
|||||||
pub webdav_username: Option<String>,
|
pub webdav_username: Option<String>,
|
||||||
pub webdav_password: Option<String>,
|
pub webdav_password: Option<String>,
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
pub enable_tray_icon: Option<bool>,
|
||||||
pub enable_auto_light_weight_mode: Option<bool>,
|
pub enable_auto_light_weight_mode: Option<bool>,
|
||||||
pub auto_light_weight_minutes: Option<u64>,
|
pub auto_light_weight_minutes: Option<u64>,
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
pub home_cards: Option<serde_json::Value>,
|
pub home_cards: Option<serde_json::Value>,
|
||||||
|
pub service_state: Option<crate::core::service::ServiceState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IVerge> for IVergeResponse {
|
impl From<IVerge> for IVergeResponse {
|
||||||
@@ -539,10 +550,12 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
webdav_username: verge.webdav_username,
|
webdav_username: verge.webdav_username,
|
||||||
webdav_password: verge.webdav_password,
|
webdav_password: verge.webdav_password,
|
||||||
enable_tray_speed: verge.enable_tray_speed,
|
enable_tray_speed: verge.enable_tray_speed,
|
||||||
|
enable_tray_icon: verge.enable_tray_icon,
|
||||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||||
enable_dns_settings: verge.enable_dns_settings,
|
enable_dns_settings: verge.enable_dns_settings,
|
||||||
home_cards: verge.home_cards,
|
home_cards: verge.home_cards,
|
||||||
|
service_state: verge.service_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+372
-456
@@ -2,395 +2,44 @@
|
|||||||
use crate::core::tray::Tray;
|
use crate::core::tray::Tray;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::*,
|
||||||
core::{handle, service},
|
core::{
|
||||||
log_err,
|
handle,
|
||||||
|
service::{self, is_service_available},
|
||||||
|
},
|
||||||
|
log_err, logging, logging_error,
|
||||||
module::mihomo::MihomoManager,
|
module::mihomo::MihomoManager,
|
||||||
utils::{dirs, help},
|
utils::{
|
||||||
|
dirs,
|
||||||
|
help::{self},
|
||||||
|
logging::Type,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::{process::CommandChild, ShellExt};
|
||||||
use tokio::{sync::Mutex, time::sleep};
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CoreManager {
|
pub struct CoreManager {
|
||||||
running: Arc<Mutex<bool>>,
|
running: Arc<Mutex<RunningMode>>,
|
||||||
|
child_sidecar: Arc<Mutex<Option<CommandChild>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 内核运行模式
|
/// 内核运行模式
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
|
||||||
pub enum RunningMode {
|
pub enum RunningMode {
|
||||||
/// 服务模式运行
|
/// 服务模式运行
|
||||||
Service,
|
Service,
|
||||||
/// Sidecar模式运行
|
/// Sidecar 模式运行
|
||||||
Sidecar,
|
Sidecar,
|
||||||
/// 未运行
|
/// 未运行
|
||||||
NotRunning,
|
NotRunning,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||||
|
|
||||||
impl CoreManager {
|
impl CoreManager {
|
||||||
pub fn global() -> &'static CoreManager {
|
|
||||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
|
||||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
|
||||||
running: Arc::new(Mutex::new(false)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init(&self) -> Result<()> {
|
|
||||||
log::trace!("run core start");
|
|
||||||
// 启动clash
|
|
||||||
log_err!(Self::global().start_core().await);
|
|
||||||
log::trace!("run core end");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 停止核心运行
|
|
||||||
pub async fn stop_core(&self) -> Result<()> {
|
|
||||||
let mut running = self.running.lock().await;
|
|
||||||
|
|
||||||
if !*running {
|
|
||||||
log::debug!("core is not running");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭tun模式
|
|
||||||
// Create a JSON object to disable TUN mode
|
|
||||||
let disable = serde_json::json!({
|
|
||||||
"tun": {
|
|
||||||
"enable": false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
log::debug!(target: "app", "disable tun mode");
|
|
||||||
log_err!(MihomoManager::global().patch_configs(disable).await);
|
|
||||||
|
|
||||||
// 服务模式
|
|
||||||
if service::check_service().await.is_ok() {
|
|
||||||
log::info!(target: "app", "stop the core by service");
|
|
||||||
match service::stop_core_by_service().await {
|
|
||||||
Ok(_) => {
|
|
||||||
log::info!(target: "app", "core stopped successfully by service");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!(target: "app", "failed to stop core by service: {}", err);
|
|
||||||
// 服务停止失败,尝试停止可能的sidecar进程
|
|
||||||
self.stop_sidecar_process();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果没有使用服务,尝试停止sidecar进程
|
|
||||||
self.stop_sidecar_process();
|
|
||||||
}
|
|
||||||
|
|
||||||
*running = false;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 停止通过sidecar启动的进程
|
|
||||||
fn stop_sidecar_process(&self) {
|
|
||||||
if let Some(process) = handle::Handle::global().take_core_process() {
|
|
||||||
log::info!(target: "app", "stopping core process in sidecar mode");
|
|
||||||
if let Err(e) = process.kill() {
|
|
||||||
log::warn!(target: "app", "failed to kill core process: {}", e);
|
|
||||||
} else {
|
|
||||||
log::info!(target: "app", "core process stopped successfully");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 启动核心
|
|
||||||
pub async fn start_core(&self) -> Result<()> {
|
|
||||||
let mut running = self.running.lock().await;
|
|
||||||
if *running {
|
|
||||||
log::info!("core is running");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
|
||||||
|
|
||||||
// 先尝试服务模式
|
|
||||||
if service::check_service().await.is_ok() {
|
|
||||||
log::info!(target: "app", "try to run core in service mode");
|
|
||||||
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);
|
|
||||||
|
|
||||||
*running = true;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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)?;
|
|
||||||
|
|
||||||
// 启动核心进程并转入后台运行
|
|
||||||
let (_, child) = app_handle
|
|
||||||
.shell()
|
|
||||||
.sidecar(clash_core)?
|
|
||||||
.args(["-d", dirs::path_to_str(&config_dir)?, "-f", config_path_str])
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
// 保存进程ID以便后续管理
|
|
||||||
handle::Handle::global().set_core_process(child);
|
|
||||||
|
|
||||||
// 等待短暂时间确保启动成功
|
|
||||||
sleep(Duration::from_millis(300)).await;
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 使用默认配置
|
|
||||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
|
||||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
|
||||||
*Config::runtime().draft() = IRuntime {
|
|
||||||
config: Some(Config::clash().latest().0.clone()),
|
|
||||||
exists_keys: vec![],
|
|
||||||
chain_logs: Default::default(),
|
|
||||||
};
|
|
||||||
help::save_yaml(
|
|
||||||
&runtime_path,
|
|
||||||
&Config::clash().latest().0,
|
|
||||||
Some("# Clash Verge Runtime"),
|
|
||||||
)?;
|
|
||||||
handle::Handle::notice_message(msg_type, msg_content);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 切换核心
|
|
||||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
|
||||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
|
||||||
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
|
|
||||||
|
|
||||||
if !CLASH_CORES.contains(&clash_core.as_str()) {
|
|
||||||
bail!("invalid clash core name \"{clash_core}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Ok((true, _)) => {
|
|
||||||
println!("[切换内核] 配置验证通过,开始切换内核");
|
|
||||||
// 3. 验证通过后,应用内核配置并重启
|
|
||||||
Config::verge().apply();
|
|
||||||
log_err!(Config::verge().latest().save_file());
|
|
||||||
|
|
||||||
match self.restart_core().await {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("[切换内核] 内核切换成功");
|
|
||||||
Config::runtime().apply();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("[切换内核] 内核切换失败: {}", 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?;
|
|
||||||
Config::verge().apply();
|
|
||||||
log_err!(Config::verge().latest().save_file());
|
|
||||||
|
|
||||||
match self.restart_core().await {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("[切换内核] 内核切换成功(使用默认配置)");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("[切换内核] 内核切换失败: {}", err);
|
|
||||||
// 即使使用服务失败,我们也尝试使用sidecar模式启动
|
|
||||||
log::info!(target: "app", "trying sidecar mode after service failure with default config");
|
|
||||||
self.start_core().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("[切换内核] 验证过程发生错误: {}", err);
|
|
||||||
Config::verge().discard();
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 内部验证配置文件的实现
|
|
||||||
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)?;
|
|
||||||
println!("[core配置验证] 测试目录: {}", test_dir);
|
|
||||||
|
|
||||||
// 使用子进程运行clash验证配置
|
|
||||||
println!("[core配置验证] 运行子进程验证配置");
|
|
||||||
let output = app_handle
|
|
||||||
.shell()
|
|
||||||
.sidecar(clash_core)?
|
|
||||||
.args(["-t", "-d", test_dir, "-f", config_path])
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
println!("\n[core配置验证] -------- 验证结果 --------");
|
|
||||||
println!("[core配置验证] 进程退出状态: {:?}", output.status);
|
|
||||||
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
println!("[core配置验证] stderr输出:\n{}", stderr);
|
|
||||||
}
|
|
||||||
if !stdout.is_empty() {
|
|
||||||
println!("[core配置验证] stdout输出:\n{}", stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_error {
|
|
||||||
println!("[core配置验证] 发现错误,开始处理错误信息");
|
|
||||||
let error_msg = if !stdout.is_empty() {
|
|
||||||
stdout.to_string()
|
|
||||||
} else if !stderr.is_empty() {
|
|
||||||
stderr.to_string()
|
|
||||||
} else if let Some(code) = output.status.code() {
|
|
||||||
format!("验证进程异常退出,退出码: {}", code)
|
|
||||||
} else {
|
|
||||||
"验证进程被终止".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("[core配置验证] -------- 验证结束 --------\n");
|
|
||||||
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
|
||||||
} else {
|
|
||||||
println!("[core配置验证] 验证成功");
|
|
||||||
println!("[core配置验证] -------- 验证结束 --------\n");
|
|
||||||
Ok((true, String::new()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证运行时配置
|
|
||||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
|
||||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
|
||||||
let config_path = dirs::path_to_str(&config_path)?;
|
|
||||||
self.validate_config_internal(config_path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证指定的配置文件
|
|
||||||
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
|
|
||||||
} else {
|
|
||||||
match self.is_script_file(config_path) {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(err) => {
|
|
||||||
// 如果无法确定文件类型,尝试使用Clash内核验证
|
|
||||||
log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err);
|
|
||||||
return self.validate_config_internal(config_path).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查文件是否为脚本文件
|
/// 检查文件是否为脚本文件
|
||||||
fn is_script_file(&self, path: &str) -> Result<bool> {
|
fn is_script_file(&self, path: &str) -> Result<bool> {
|
||||||
// 1. 先通过扩展名快速判断
|
// 1. 先通过扩展名快速判断
|
||||||
@@ -458,7 +107,174 @@ impl CoreManager {
|
|||||||
log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path);
|
log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
/// 使用默认配置
|
||||||
|
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||||
|
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||||
|
*Config::runtime().draft() = IRuntime {
|
||||||
|
config: Some(Config::clash().latest().0.clone()),
|
||||||
|
exists_keys: vec![],
|
||||||
|
chain_logs: Default::default(),
|
||||||
|
};
|
||||||
|
help::save_yaml(
|
||||||
|
&runtime_path,
|
||||||
|
&Config::clash().latest().0,
|
||||||
|
Some("# Clash Verge Runtime"),
|
||||||
|
)?;
|
||||||
|
handle::Handle::notice_message(msg_type, msg_content);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// 验证运行时配置
|
||||||
|
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||||
|
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||||
|
let config_path = dirs::path_to_str(&config_path)?;
|
||||||
|
self.validate_config_internal(config_path).await
|
||||||
|
}
|
||||||
|
/// 验证指定的配置文件
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
match self.is_script_file(config_path) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(err) => {
|
||||||
|
// 如果无法确定文件类型,尝试使用Clash内核验证
|
||||||
|
log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err);
|
||||||
|
return self.validate_config_internal(config_path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
/// 内部验证配置文件的实现
|
||||||
|
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)?;
|
||||||
|
println!("[core配置验证] 测试目录: {}", test_dir);
|
||||||
|
|
||||||
|
// 使用子进程运行clash验证配置
|
||||||
|
println!("[core配置验证] 运行子进程验证配置");
|
||||||
|
let output = app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar(clash_core)?
|
||||||
|
.args(["-t", "-d", test_dir, "-f", config_path])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
println!("\n[core配置验证] -------- 验证结果 --------");
|
||||||
|
println!("[core配置验证] 进程退出状态: {:?}", output.status);
|
||||||
|
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
println!("[core配置验证] stderr输出:\n{}", stderr);
|
||||||
|
}
|
||||||
|
if !stdout.is_empty() {
|
||||||
|
println!("[core配置验证] stdout输出:\n{}", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_error {
|
||||||
|
println!("[core配置验证] 发现错误,开始处理错误信息");
|
||||||
|
let error_msg = if !stdout.is_empty() {
|
||||||
|
stdout.to_string()
|
||||||
|
} else if !stderr.is_empty() {
|
||||||
|
stderr.to_string()
|
||||||
|
} else if let Some(code) = output.status.code() {
|
||||||
|
format!("验证进程异常退出,退出码: {}", code)
|
||||||
|
} else {
|
||||||
|
"验证进程被终止".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("[core配置验证] -------- 验证结束 --------\n");
|
||||||
|
Ok((false, error_msg)) // 返回错误消息给调用者处理
|
||||||
|
} else {
|
||||||
|
println!("[core配置验证] 验证成功");
|
||||||
|
println!("[core配置验证] -------- 验证结束 --------\n");
|
||||||
|
Ok((true, String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 只进行文件语法检查,不进行完整验证
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// 验证脚本文件语法
|
/// 验证脚本文件语法
|
||||||
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
||||||
// 读取脚本内容
|
// 读取脚本内容
|
||||||
@@ -505,7 +321,6 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新proxies等配置
|
/// 更新proxies等配置
|
||||||
pub async fn update_config(&self) -> Result<(bool, String)> {
|
pub async fn update_config(&self) -> Result<(bool, String)> {
|
||||||
// 检查程序是否正在退出,如果是则跳过完整验证流程
|
// 检查程序是否正在退出,如果是则跳过完整验证流程
|
||||||
@@ -533,31 +348,8 @@ impl CoreManager {
|
|||||||
// 4. 验证通过后,生成正式的运行时配置
|
// 4. 验证通过后,生成正式的运行时配置
|
||||||
println!("[core配置更新] 生成运行时配置");
|
println!("[core配置更新] 生成运行时配置");
|
||||||
let run_path = Config::generate_file(ConfigType::Run)?;
|
let run_path = Config::generate_file(ConfigType::Run)?;
|
||||||
let run_path = dirs::path_to_str(&run_path)?;
|
logging_error!(Type::Core, true, self.put_configs_force(run_path).await);
|
||||||
|
Ok((true, "something".into()))
|
||||||
// 5. 应用新配置
|
|
||||||
println!("[core配置更新] 应用新配置");
|
|
||||||
for i in 0..3 {
|
|
||||||
match MihomoManager::global().put_configs_force(run_path).await {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("[core配置更新] 配置应用成功");
|
|
||||||
Config::runtime().apply();
|
|
||||||
return Ok((true, String::new()));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
if i < 2 {
|
|
||||||
println!("[core配置更新] 第{}次重试应用配置", i + 1);
|
|
||||||
log::info!(target: "app", "{err}");
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
} else {
|
|
||||||
println!("[core配置更新] 配置应用失败: {}", err);
|
|
||||||
Config::runtime().discard();
|
|
||||||
return Ok((false, err.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((true, String::new()))
|
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
println!("[core配置更新] 配置验证失败: {}", error_msg);
|
println!("[core配置更新] 配置验证失败: {}", error_msg);
|
||||||
@@ -571,68 +363,192 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub async fn put_configs_force(&self, path_buf: PathBuf) -> Result<(), String> {
|
||||||
/// 只进行文件语法检查,不进行完整验证
|
let run_path_str = dirs::path_to_str(&path_buf).map_err(|e| {
|
||||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
let msg = e.to_string();
|
||||||
println!("[core配置语法检查] 开始检查文件: {}", config_path);
|
logging_error!(Type::Core, true, "{}", msg);
|
||||||
|
msg
|
||||||
// 读取文件内容
|
});
|
||||||
let content = match std::fs::read_to_string(config_path) {
|
match MihomoManager::global()
|
||||||
Ok(content) => content,
|
.put_configs_force(run_path_str?)
|
||||||
Err(err) => {
|
.await
|
||||||
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(_) => {
|
Ok(_) => {
|
||||||
println!("[core配置语法检查] YAML语法检查通过");
|
Config::runtime().apply();
|
||||||
Ok((true, String::new()))
|
logging!(info, Type::Core, true, "Configuration updated successfully");
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(e) => {
|
||||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
let msg = e.to_string();
|
||||||
let error_msg = format!("YAML syntax error: {}", err);
|
Config::runtime().discard();
|
||||||
println!("[core配置语法检查] YAML语法错误: {}", error_msg);
|
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
|
||||||
Ok((false, error_msg))
|
return Err(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() {
|
|
||||||
RunningMode::Sidecar
|
|
||||||
} else {
|
|
||||||
RunningMode::NotRunning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// 服务不可用,检查是否有sidecar进程
|
|
||||||
if handle::Handle::global().has_core_process() {
|
|
||||||
RunningMode::Sidecar
|
|
||||||
} else {
|
|
||||||
RunningMode::NotRunning
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CoreManager {
|
||||||
|
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||||
|
logging!(trace, Type::Core, true, "Running core by sidecar");
|
||||||
|
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||||
|
let app_handle = handle::Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||||
|
let clash_core = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.clash_core
|
||||||
|
.clone()
|
||||||
|
.unwrap_or("verge-mihomo".to_string());
|
||||||
|
let config_dir = dirs::app_home_dir()?;
|
||||||
|
let (_, child) = app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar(&clash_core)?
|
||||||
|
.args([
|
||||||
|
"-d",
|
||||||
|
dirs::path_to_str(&config_dir)?,
|
||||||
|
"-f",
|
||||||
|
dirs::path_to_str(&config_file)?,
|
||||||
|
])
|
||||||
|
.spawn()?;
|
||||||
|
let pid = child.pid();
|
||||||
|
logging!(
|
||||||
|
trace,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"Started core by sidecar pid: {}",
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
*self.child_sidecar.lock().await = Some(child);
|
||||||
|
self.set_running_mode(RunningMode::Sidecar).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn stop_core_by_sidecar(&self) -> Result<()> {
|
||||||
|
logging!(trace, Type::Core, true, "Stopping core by sidecar");
|
||||||
|
|
||||||
|
if let Some(child) = self.child_sidecar.lock().await.take() {
|
||||||
|
let pid = child.pid();
|
||||||
|
child.kill()?;
|
||||||
|
logging!(
|
||||||
|
trace,
|
||||||
|
Type::Core,
|
||||||
|
true,
|
||||||
|
"Stopped core by sidecar pid: {}",
|
||||||
|
pid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.set_running_mode(RunningMode::NotRunning).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreManager {
|
||||||
|
async fn start_core_by_service(&self) -> Result<()> {
|
||||||
|
logging!(trace, Type::Core, true, "Running core by service");
|
||||||
|
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||||
|
service::run_core_by_service(config_file).await?;
|
||||||
|
self.set_running_mode(RunningMode::Service).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn stop_core_by_service(&self) -> Result<()> {
|
||||||
|
logging!(trace, Type::Core, true, "Stopping core by service");
|
||||||
|
service::stop_core_by_service().await?;
|
||||||
|
self.set_running_mode(RunningMode::NotRunning).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreManager {
|
||||||
|
pub fn global() -> &'static CoreManager {
|
||||||
|
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||||
|
CORE_MANAGER.get_or_init(|| CoreManager {
|
||||||
|
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
|
||||||
|
child_sidecar: Arc::new(Mutex::new(None)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(&self) -> Result<()> {
|
||||||
|
logging!(trace, Type::Core, "Initializing core");
|
||||||
|
if is_service_available().await.is_ok() {
|
||||||
|
Self::global().start_core_by_service().await?;
|
||||||
|
} else {
|
||||||
|
Self::global().start_core_by_sidecar().await?;
|
||||||
|
}
|
||||||
|
logging!(trace, Type::Core, "Initied core");
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
log_err!(Tray::global().subscribe_traffic().await);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_running_mode(&self, mode: RunningMode) {
|
||||||
|
let mut guard = self.running.lock().await;
|
||||||
|
*guard = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_running_mode(&self) -> RunningMode {
|
||||||
|
let guard = self.running.lock().await;
|
||||||
|
(*guard).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动核心
|
||||||
|
pub async fn start_core(&self) -> Result<()> {
|
||||||
|
if is_service_available().await.is_ok() {
|
||||||
|
self.start_core_by_service().await?;
|
||||||
|
} else {
|
||||||
|
self.start_core_by_sidecar().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止核心运行
|
||||||
|
pub async fn stop_core(&self) -> Result<()> {
|
||||||
|
match self.get_running_mode().await {
|
||||||
|
RunningMode::Service => self.stop_core_by_service().await,
|
||||||
|
RunningMode::Sidecar => self.stop_core_by_sidecar().await,
|
||||||
|
RunningMode::NotRunning => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重启内核
|
||||||
|
pub async fn restart_core(&self) -> Result<()> {
|
||||||
|
self.stop_core().await?;
|
||||||
|
self.start_core().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强制重新安装服务(供UI调用,用户主动修复服务)
|
||||||
|
pub async fn repair_service(&self) -> Result<()> {
|
||||||
|
service::force_reinstall_service().await?;
|
||||||
|
self.restart_core().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换核心
|
||||||
|
pub async fn change_core(&self, clash_core: Option<String>) -> Result<(), String> {
|
||||||
|
if clash_core.is_none() {
|
||||||
|
let error_message = "Clash core should not be Null";
|
||||||
|
logging!(error, Type::Core, true, "{}", error_message);
|
||||||
|
return Err(error_message.to_string());
|
||||||
|
}
|
||||||
|
let core: &str = &clash_core.clone().unwrap();
|
||||||
|
if !CLASH_CORES.contains(&core) {
|
||||||
|
let error_message = format!("Clash core invalid name: {}", core);
|
||||||
|
logging!(error, Type::Core, true, "{}", error_message);
|
||||||
|
return Err(error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Config::verge().draft().clash_core = clash_core.clone();
|
||||||
|
Config::verge().apply();
|
||||||
|
logging_error!(Type::Core, true, Config::verge().latest().save_file());
|
||||||
|
|
||||||
|
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| {
|
||||||
|
let msg = e.to_string();
|
||||||
|
logging_error!(Type::Core, true, "{}", msg);
|
||||||
|
msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.put_configs_force(run_path).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ use once_cell::sync::OnceCell;
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||||
use tauri_plugin_shell::process::CommandChild;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||||
pub is_exiting: Arc<RwLock<bool>>,
|
pub is_exiting: Arc<RwLock<bool>>,
|
||||||
pub core_process: Arc<RwLock<Option<CommandChild>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
@@ -19,7 +17,6 @@ impl Handle {
|
|||||||
HANDLE.get_or_init(|| Handle {
|
HANDLE.get_or_init(|| Handle {
|
||||||
app_handle: Arc::new(RwLock::new(None)),
|
app_handle: Arc::new(RwLock::new(None)),
|
||||||
is_exiting: Arc::new(RwLock::new(false)),
|
is_exiting: Arc::new(RwLock::new(false)),
|
||||||
core_process: Arc::new(RwLock::new(None)),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,21 +68,6 @@ impl Handle {
|
|||||||
*is_exiting = true;
|
*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 {
|
pub fn is_exiting(&self) -> bool {
|
||||||
*self.is_exiting.read()
|
*self.is_exiting.read()
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-49
@@ -1,4 +1,10 @@
|
|||||||
use crate::{config::Config, core::handle, feat, log_err, utils::resolve};
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
core::handle,
|
||||||
|
feat, log_err, logging,
|
||||||
|
module::lightweight::entry_lightweight_mode,
|
||||||
|
utils::{logging::Type, resolve},
|
||||||
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -23,22 +29,27 @@ impl Hotkey {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
println!(
|
logging!(
|
||||||
"Initializing hotkeys, global hotkey enabled: {}",
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Initializing hotkeys with enable: {}",
|
||||||
enable_global_hotkey
|
enable_global_hotkey
|
||||||
);
|
);
|
||||||
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
|
||||||
|
|
||||||
// 如果全局热键被禁用,则不注册热键
|
// 如果全局热键被禁用,则不注册热键
|
||||||
if !enable_global_hotkey {
|
if !enable_global_hotkey {
|
||||||
println!("Global hotkey is disabled, skipping registration");
|
|
||||||
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||||
println!("Found {} hotkeys to register", hotkeys.len());
|
logging!(
|
||||||
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Has {} hotkeys need to register",
|
||||||
|
hotkeys.len()
|
||||||
|
);
|
||||||
|
|
||||||
for hotkey in hotkeys.iter() {
|
for hotkey in hotkeys.iter() {
|
||||||
let mut iter = hotkey.split(',');
|
let mut iter = hotkey.split(',');
|
||||||
@@ -47,28 +58,52 @@ impl Hotkey {
|
|||||||
|
|
||||||
match (key, func) {
|
match (key, func) {
|
||||||
(Some(key), Some(func)) => {
|
(Some(key), Some(func)) => {
|
||||||
println!("Registering hotkey: {} -> {}", key, func);
|
logging!(
|
||||||
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Registering hotkey: {} -> {}",
|
||||||
|
key,
|
||||||
|
func
|
||||||
|
);
|
||||||
if let Err(e) = self.register(key, func) {
|
if let Err(e) = self.register(key, func) {
|
||||||
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
logging!(
|
||||||
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Failed to register hotkey {} -> {}: {:?}",
|
||||||
|
key,
|
||||||
|
func,
|
||||||
|
e
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("Successfully registered hotkey {} -> {}", key, func);
|
logging!(
|
||||||
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Successfully registered hotkey {} -> {}",
|
||||||
|
key,
|
||||||
|
func
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let key = key.unwrap_or("None");
|
let key = key.unwrap_or("None");
|
||||||
let func = func.unwrap_or("None");
|
let func = func.unwrap_or("None");
|
||||||
println!("Invalid hotkey configuration: `{key}`:`{func}`");
|
logging!(
|
||||||
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Invalid hotkey configuration: `{}`:`{}`",
|
||||||
|
key,
|
||||||
|
func
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.current.lock().clone_from(hotkeys);
|
self.current.lock().clone_from(hotkeys);
|
||||||
} else {
|
} else {
|
||||||
println!("No hotkeys configured");
|
logging!(info, Type::Hotkey, true, "No hotkeys configured");
|
||||||
log::info!(target: "app", "No hotkeys configured");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -85,45 +120,60 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
|
|
||||||
println!(
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
"Attempting to register hotkey: {} for function: {}",
|
"Attempting to register hotkey: {} for function: {}",
|
||||||
hotkey, func
|
hotkey,
|
||||||
|
func
|
||||||
);
|
);
|
||||||
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
|
||||||
|
|
||||||
if manager.is_registered(hotkey) {
|
if manager.is_registered(hotkey) {
|
||||||
println!(
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
"Hotkey {} was already registered, unregistering first",
|
"Hotkey {} was already registered, unregistering first",
|
||||||
hotkey
|
hotkey
|
||||||
);
|
);
|
||||||
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
|
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = match func.trim() {
|
let f = match func.trim() {
|
||||||
"open_or_close_dashboard" => {
|
"open_or_close_dashboard" => {
|
||||||
println!("Registering open_or_close_dashboard function");
|
logging!(
|
||||||
log::info!(target: "app", "Registering open_or_close_dashboard function");
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Registering open_or_close_dashboard function"
|
||||||
|
);
|
||||||
|| {
|
|| {
|
||||||
println!("=== Hotkey Dashboard Window Operation Start ===");
|
logging!(
|
||||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"=== Hotkey Dashboard Window Operation Start ==="
|
||||||
|
);
|
||||||
|
|
||||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||||
async_runtime::spawn_blocking(|| {
|
async_runtime::spawn_blocking(|| {
|
||||||
println!("Toggle dashboard window visibility");
|
logging!(
|
||||||
log::info!(target: "app", "Toggle dashboard window visibility");
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Toggle dashboard window visibility"
|
||||||
|
);
|
||||||
|
|
||||||
// 检查窗口是否存在
|
// 检查窗口是否存在
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
// 如果窗口可见,则隐藏它
|
// 如果窗口可见,则隐藏它
|
||||||
if window.is_visible().unwrap_or(false) {
|
if window.is_visible().unwrap_or(false) {
|
||||||
println!("Window is visible, hiding it");
|
logging!(info, Type::Window, true, "Window is visible, hiding it");
|
||||||
log::info!(target: "app", "Window is visible, hiding it");
|
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
} else {
|
} else {
|
||||||
// 如果窗口不可见,则显示它
|
// 如果窗口不可见,则显示它
|
||||||
println!("Window is hidden, showing it");
|
logging!(info, Type::Window, true, "Window is hidden, showing it");
|
||||||
log::info!(target: "app", "Window is hidden, showing it");
|
|
||||||
if window.is_minimized().unwrap_or(false) {
|
if window.is_minimized().unwrap_or(false) {
|
||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
}
|
}
|
||||||
@@ -132,14 +182,22 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果窗口不存在,创建一个新窗口
|
// 如果窗口不存在,创建一个新窗口
|
||||||
println!("Window does not exist, creating a new one");
|
logging!(
|
||||||
log::info!(target: "app", "Window does not exist, creating a new one");
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Window does not exist, creating a new one"
|
||||||
|
);
|
||||||
resolve::create_window();
|
resolve::create_window();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("=== Hotkey Dashboard Window Operation End ===");
|
logging!(
|
||||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"=== Hotkey Dashboard Window Operation End ==="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||||
@@ -147,13 +205,13 @@ impl Hotkey {
|
|||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||||
|
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||||
"quit" => || feat::quit(Some(0)),
|
"quit" => || feat::quit(Some(0)),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
"hide" => || feat::hide(),
|
"hide" => || feat::hide(),
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid function: {}", func);
|
logging!(error, Type::Hotkey, true, "Invalid function: {}", func);
|
||||||
log::error!(target: "app", "Invalid function: {}", func);
|
|
||||||
bail!("invalid function \"{func}\"");
|
bail!("invalid function \"{func}\"");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -162,21 +220,18 @@ impl Hotkey {
|
|||||||
|
|
||||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
println!("Hotkey pressed: {:?}", hotkey);
|
logging!(info, Type::Hotkey, true, "Hotkey pressed: {:?}", hotkey);
|
||||||
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
|
|
||||||
|
|
||||||
if hotkey.key == Code::KeyQ && is_quit {
|
if hotkey.key == Code::KeyQ && is_quit {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
if window.is_focused().unwrap_or(false) {
|
if window.is_focused().unwrap_or(false) {
|
||||||
println!("Executing quit function");
|
logging!(info, Type::Hotkey, true, "Executing quit function");
|
||||||
log::info!(target: "app", "Executing quit function");
|
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 直接执行函数,不做任何状态检查
|
// 直接执行函数,不做任何状态检查
|
||||||
println!("Executing function directly");
|
logging!(info, Type::Hotkey, true, "Executing function directly");
|
||||||
log::info!(target: "app", "Executing function directly");
|
|
||||||
|
|
||||||
// 获取全局热键状态
|
// 获取全局热键状态
|
||||||
let is_enable_global_hotkey = Config::verge()
|
let is_enable_global_hotkey = Config::verge()
|
||||||
@@ -199,8 +254,14 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("Successfully registered hotkey {} for {}", hotkey, func);
|
logging!(
|
||||||
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
|
info,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Successfully registered hotkey {} for {}",
|
||||||
|
hotkey,
|
||||||
|
func
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +269,7 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
logging!(debug, Type::Hotkey, true, "Unregister hotkey {}", hotkey);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +342,13 @@ impl Drop for Hotkey {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||||
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Error unregistering all hotkeys: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+294
-13
@@ -1,13 +1,94 @@
|
|||||||
use crate::{config::Config, utils::dirs};
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
logging,
|
||||||
|
utils::{dirs, logging::Type},
|
||||||
|
};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, env::current_exe, path::PathBuf, 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;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
|
|
||||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||||
const REQUIRED_SERVICE_VERSION: &str = "1.0.2"; // 定义所需的服务版本号
|
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)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct ResponseBody {
|
pub struct ResponseBody {
|
||||||
@@ -41,6 +122,15 @@ pub struct VersionJsonResponse {
|
|||||||
pub async fn reinstall_service() -> Result<()> {
|
pub async fn reinstall_service() -> Result<()> {
|
||||||
log::info!(target:"app", "reinstall service");
|
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 deelevate::{PrivilegeLevel, Token};
|
||||||
use runas::Command as RunasCommand;
|
use runas::Command as RunasCommand;
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
@@ -74,12 +164,20 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
bail!(
|
let error = format!(
|
||||||
"failed to install service with status {}",
|
"failed to install service with status {}",
|
||||||
status.code().unwrap()
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +228,12 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录安装信息并保存
|
||||||
|
let mut service_state = ServiceState::get();
|
||||||
|
service_state.record_install();
|
||||||
|
service_state.last_error = None;
|
||||||
|
service_state.save()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +286,13 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
status.code().unwrap()
|
status.code().unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录安装信息并保存
|
||||||
|
let mut service_state = ServiceState::get();
|
||||||
|
service_state.record_install();
|
||||||
|
service_state.last_error = None;
|
||||||
|
service_state.save()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,19 +337,54 @@ pub async fn check_service_version() -> Result<String> {
|
|||||||
|
|
||||||
/// check if service needs to be reinstalled
|
/// check if service needs to be reinstalled
|
||||||
pub async fn check_service_needs_reinstall() -> bool {
|
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 {
|
match check_service_version().await {
|
||||||
Ok(version) => version != REQUIRED_SERVICE_VERSION,
|
Ok(version) => {
|
||||||
Err(_) => true, // 如果无法获取版本或服务未运行,也需要重新安装
|
// 打印更详细的日志,方便排查问题
|
||||||
|
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 // 服务不可用,需要重装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// start the clash by service
|
/// 尝试使用现有服务启动核心,不进行重装
|
||||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||||
// 检查服务版本,如果不匹配则重新安装
|
log::info!(target:"app", "attempting to start core with existing service");
|
||||||
if check_service_needs_reinstall().await {
|
|
||||||
log::info!(target: "app", "service version mismatch, reinstalling");
|
|
||||||
reinstall_service().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||||
@@ -250,6 +396,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
|
|
||||||
let config_dir = dirs::app_home_dir()?;
|
let config_dir = dirs::app_home_dir()?;
|
||||||
let config_dir = dirs::path_to_str(&config_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::service_log_file()?;
|
||||||
let log_path = dirs::path_to_str(&log_path)?;
|
let log_path = dirs::path_to_str(&log_path)?;
|
||||||
@@ -278,6 +426,106 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
Ok(())
|
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
|
/// stop the clash by service
|
||||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||||
let url = format!("{SERVICE_URL}/stop_clash");
|
let url = format!("{SERVICE_URL}/stop_clash");
|
||||||
@@ -298,8 +546,41 @@ pub async fn is_service_running() -> Result<bool> {
|
|||||||
|
|
||||||
// 检查服务状态码和消息
|
// 检查服务状态码和消息
|
||||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||||
|
logging!(debug, Type::Service, "Service is running");
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
|
logging!(debug, Type::Service, "Service is not running");
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_service_available() -> Result<()> {
|
||||||
|
let resp = check_service().await?;
|
||||||
|
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||||
|
logging!(debug, Type::Service, "Service is available");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强制重装服务(用于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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
cmd,
|
cmd,
|
||||||
config::Config,
|
config::Config,
|
||||||
feat,
|
feat,
|
||||||
module::mihomo::Rate,
|
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
|
||||||
resolve,
|
resolve,
|
||||||
utils::{dirs, i18n::t, resolve::VERSION},
|
utils::{dirs, i18n::t, resolve::VERSION},
|
||||||
};
|
};
|
||||||
@@ -21,6 +21,10 @@ use parking_lot::RwLock;
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use speed_rate::{SpeedRate, Traffic};
|
pub use speed_rate::{SpeedRate, Traffic};
|
||||||
#[cfg(target_os = "macos")]
|
#[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 std::sync::Arc;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||||
@@ -36,6 +40,9 @@ pub struct Tray {
|
|||||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||||
is_subscribed: Arc<RwLock<bool>>,
|
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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -50,6 +57,9 @@ impl Tray {
|
|||||||
speed_rate: Arc::new(Mutex::new(None)),
|
speed_rate: Arc::new(Mutex::new(None)),
|
||||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||||
is_subscribed: Arc::new(RwLock::new(false)),
|
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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -84,6 +94,7 @@ impl Tray {
|
|||||||
tray.on_tray_icon_event(|_, event| {
|
tray.on_tray_icon_event(|_, event| {
|
||||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||||
|
log::debug!(target: "app","tray event: {:?}", tray_event);
|
||||||
|
|
||||||
if let TrayIconEvent::Click {
|
if let TrayIconEvent::Click {
|
||||||
button: MouseButton::Left,
|
button: MouseButton::Left,
|
||||||
@@ -228,34 +239,66 @@ impl Tray {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[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 is_colorful = tray_icon == "colorful";
|
||||||
|
|
||||||
// 处理图标和速率
|
let icon_hash = {
|
||||||
let final_icon_bytes = if enable_tray_speed {
|
let mut hasher = DefaultHasher::new();
|
||||||
let rate = rate.or_else(|| {
|
icon_bytes.clone().hash(&mut hasher);
|
||||||
self.speed_rate
|
hasher.finish()
|
||||||
.lock()
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用新的方法渲染图标和速率
|
|
||||||
SpeedRate::add_speed_text(icon_bytes, rate)?
|
|
||||||
} else {
|
|
||||||
icon_bytes
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置系统托盘图标
|
let mut icon_hash_guard = self.icon_hash.lock();
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
let mut icon_bytes_guard = self.icon_cache.lock();
|
||||||
// 只对单色图标使用 template 模式
|
if *icon_hash_guard != Some(icon_hash) {
|
||||||
let _ = tray.set_icon_as_template(!is_colorful);
|
*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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
{
|
||||||
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘提示
|
/// 更新托盘提示
|
||||||
@@ -490,6 +533,15 @@ fn create_tray_menu(
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.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 =
|
let copy_env =
|
||||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||||
|
|
||||||
@@ -582,6 +634,8 @@ fn create_tray_menu(
|
|||||||
separator,
|
separator,
|
||||||
system_proxy,
|
system_proxy,
|
||||||
tun_mode,
|
tun_mode,
|
||||||
|
separator,
|
||||||
|
lighteweight_mode,
|
||||||
copy_env,
|
copy_env,
|
||||||
open_dir,
|
open_dir,
|
||||||
more,
|
more,
|
||||||
@@ -609,6 +663,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
|
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
|
||||||
"restart_clash" => feat::restart_clash_core(),
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
"restart_app" => feat::restart_app(),
|
"restart_app" => feat::restart_app(),
|
||||||
|
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||||
"quit" => {
|
"quit" => {
|
||||||
println!("quit");
|
println!("quit");
|
||||||
feat::quit(Some(0));
|
feat::quit(Some(0));
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use tungstenite::client::IntoClientRequest;
|
|||||||
pub struct SpeedRate {
|
pub struct SpeedRate {
|
||||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||||
last_update: Arc<Mutex<std::time::Instant>>,
|
last_update: Arc<Mutex<std::time::Instant>>,
|
||||||
// 移除 base_image,不再缓存原始图像
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpeedRate {
|
impl SpeedRate {
|
||||||
@@ -77,29 +76,51 @@ impl SpeedRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分离图标加载和速率渲染
|
// 分离图标加载和速率渲染
|
||||||
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
pub fn add_speed_text<'a>(
|
||||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
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 = image::load_from_memory(&icon_bytes)?;
|
let icon_image = if let Some(bytes) = icon_bytes {
|
||||||
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
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 =
|
let is_colorful = if let Some(bytes) = icon_bytes {
|
||||||
!crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
!crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
// 增加文本宽度和间距
|
// 增加文本宽度和间距
|
||||||
let text_width = 580; // 文本区域宽度
|
let total_width = if icon_bytes.is_some() {
|
||||||
let total_width = icon_width + text_width;
|
if icon_width < 580 {
|
||||||
|
icon_width + 580
|
||||||
|
} else {
|
||||||
|
icon_width
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
580
|
||||||
|
};
|
||||||
|
|
||||||
// 创建新的透明画布
|
// 创建新的透明画布
|
||||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||||
|
|
||||||
// 将原始图标绘制到新画布的左侧
|
// 将原始图标绘制到新画布的左侧
|
||||||
for y in 0..icon_height {
|
if icon_bytes.is_some() {
|
||||||
for x in 0..icon_width {
|
for y in 0..icon_height {
|
||||||
let pixel = icon_image.get_pixel(x, y);
|
for x in 0..icon_width {
|
||||||
combined_image.put_pixel(x, y, pixel);
|
let pixel = icon_image.get_pixel(x, y);
|
||||||
|
combined_image.put_pixel(x, y, pixel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ pub fn change_clash_mode(mode: String) {
|
|||||||
});
|
});
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
log::debug!(target: "app", "change clash mode to {mode}");
|
log::debug!(target: "app", "change clash mode to {mode}");
|
||||||
|
|
||||||
match MihomoManager::global().patch_configs(json_value).await {
|
match MihomoManager::global().patch_configs(json_value).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// 更新订阅
|
// 更新订阅
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
let http_enabled = patch.verge_http_enabled;
|
let http_enabled = patch.verge_http_enabled;
|
||||||
let http_port = patch.verge_port;
|
let http_port = patch.verge_port;
|
||||||
let enable_tray_speed = patch.enable_tray_speed;
|
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 enable_global_hotkey = patch.enable_global_hotkey;
|
||||||
let tray_event = patch.tray_event;
|
let tray_event = patch.tray_event;
|
||||||
let home_cards = patch.home_cards.clone();
|
let home_cards = patch.home_cards.clone();
|
||||||
@@ -145,6 +146,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
|| tun_tray_icon.is_some()
|
|| tun_tray_icon.is_some()
|
||||||
|| tray_icon.is_some()
|
|| tray_icon.is_some()
|
||||||
|| enable_tray_speed.is_some()
|
|| enable_tray_speed.is_some()
|
||||||
|
|| enable_tray_icon.is_some()
|
||||||
{
|
{
|
||||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||||
}
|
}
|
||||||
@@ -164,6 +166,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
|
|
||||||
// Process updates based on flags
|
// Process updates based on flags
|
||||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||||
|
Config::generate().await?;
|
||||||
CoreManager::global().restart_core().await?;
|
CoreManager::global().restart_core().await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ pub fn run() {
|
|||||||
// 添加新的命令
|
// 添加新的命令
|
||||||
cmd::get_running_mode,
|
cmd::get_running_mode,
|
||||||
cmd::install_service,
|
cmd::install_service,
|
||||||
|
cmd::repair_service,
|
||||||
cmd::get_app_uptime,
|
cmd::get_app_uptime,
|
||||||
cmd::get_auto_launch_status,
|
cmd::get_auto_launch_status,
|
||||||
// clash
|
// clash
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use tauri::{Listener, Manager};
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, timer::Timer},
|
core::{handle, timer::Timer},
|
||||||
log_err,
|
log_err, AppHandleManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||||
@@ -25,18 +25,19 @@ pub fn disable_auto_light_weight_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry_lightweight_mode() {
|
pub fn entry_lightweight_mode() {
|
||||||
println!("尝试进入轻量模式。motherfucker");
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
log_err!(window.close());
|
if window.is_visible().unwrap_or(false) {
|
||||||
}
|
let _ = window.hide();
|
||||||
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
|
||||||
if let Some(webview) = window.get_webview_window("main") {
|
|
||||||
log_err!(webview.destroy());
|
|
||||||
println!("[lightweight_mode] 轻量模式已开启");
|
|
||||||
log::info!(target: "app", "[lightweight_mode] 轻量模式已开启");
|
|
||||||
}
|
}
|
||||||
|
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 {
|
fn setup_window_close_listener() -> u32 {
|
||||||
@@ -132,10 +133,9 @@ fn cancel_light_weight_timer() -> Result<()> {
|
|||||||
delay_timer
|
delay_timer
|
||||||
.remove_task(task.task_id)
|
.remove_task(task.task_id)
|
||||||
.context("failed to remove light weight timer task")?;
|
.context("failed to remove light weight timer task")?;
|
||||||
|
println!("[lightweight_mode] 轻量模式计时器已取消");
|
||||||
|
log::info!(target: "app", "[lightweight_mode] 轻量模式计时器已取消");
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("[lightweight_mode] 轻量模式计时器已取消");
|
|
||||||
log::info!(target: "app", "[lightweight_mode] 轻量模式计时器已取消");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ impl PlatformSpecification {
|
|||||||
tokio::runtime::Handle::current().block_on(async {
|
tokio::runtime::Handle::current().block_on(async {
|
||||||
match CoreManager::global().get_running_mode().await {
|
match CoreManager::global().get_running_mode().await {
|
||||||
crate::core::RunningMode::Service => "Service".to_string(),
|
crate::core::RunningMode::Service => "Service".to_string(),
|
||||||
crate::core::RunningMode::Sidecar => "Sidecar".to_string(),
|
crate::core::RunningMode::Sidecar => "Standalone".to_string(),
|
||||||
crate::core::RunningMode::NotRunning => "Not Running".to_string(),
|
crate::core::RunningMode::NotRunning => "Not Running".to_string(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -136,52 +136,6 @@ pub fn linux_elevator() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! error {
|
|
||||||
($result: expr) => {
|
|
||||||
log::error!(target: "app", "{}", $result);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! log_err {
|
|
||||||
($result: expr) => {
|
|
||||||
if let Err(err) = $result {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
($result: expr, $err_str: expr) => {
|
|
||||||
if let Err(_) = $result {
|
|
||||||
log::error!(target: "app", "{}", $err_str);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! trace_err {
|
|
||||||
($result: expr, $err_str: expr) => {
|
|
||||||
if let Err(err) = $result {
|
|
||||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// wrap the anyhow error
|
|
||||||
/// transform the error to String
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! wrap_err {
|
|
||||||
($stat: expr) => {
|
|
||||||
match $stat {
|
|
||||||
Ok(a) => Ok(a),
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{}", err.to_string());
|
|
||||||
Err(format!("{}", err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return the string literal error
|
/// return the string literal error
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ret_err {
|
macro_rules! ret_err {
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Type {
|
||||||
|
Core,
|
||||||
|
Service,
|
||||||
|
Hotkey,
|
||||||
|
Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Type {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Type::Core => write!(f, "[Core]"),
|
||||||
|
Type::Service => write!(f, "[Service]"),
|
||||||
|
Type::Hotkey => write!(f, "[Hotkey]"),
|
||||||
|
Type::Window => write!(f, "[Window]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! error {
|
||||||
|
($result: expr) => {
|
||||||
|
log::error!(target: "app", "{}", $result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_err {
|
||||||
|
($result: expr) => {
|
||||||
|
if let Err(err) = $result {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($result: expr, $err_str: expr) => {
|
||||||
|
if let Err(_) = $result {
|
||||||
|
log::error!(target: "app", "{}", $err_str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! trace_err {
|
||||||
|
($result: expr, $err_str: expr) => {
|
||||||
|
if let Err(err) = $result {
|
||||||
|
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wrap the anyhow error
|
||||||
|
/// transform the error to String
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! wrap_err {
|
||||||
|
($stat: expr) => {
|
||||||
|
match $stat {
|
||||||
|
Ok(a) => Ok(a),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{}", err.to_string());
|
||||||
|
Err(format!("{}", err.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! logging {
|
||||||
|
// 带 println 的版本(支持格式化参数)
|
||||||
|
($level:ident, $type:expr, $print:expr, $($arg:tt)*) => {
|
||||||
|
println!("{} {}", $type, format_args!($($arg)*));
|
||||||
|
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 不带 println 的版本
|
||||||
|
($level:ident, $type:expr, $($arg:tt)*) => {
|
||||||
|
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! logging_error {
|
||||||
|
// Version with println and Result expression
|
||||||
|
($type:expr, $print:expr, $expr:expr) => {
|
||||||
|
match $expr {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(err) => {
|
||||||
|
if $print {
|
||||||
|
println!("[{}] Error: {}", $type, err);
|
||||||
|
}
|
||||||
|
log::error!(target: "app", "{} {}", $type, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version without println and Result expression
|
||||||
|
($type:expr, $expr:expr) => {
|
||||||
|
if let Err(err) = $expr {
|
||||||
|
log::error!(target: "app", "{} {}", $type, err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version with println and custom message
|
||||||
|
($type:expr, $print:expr, $($arg:tt)*) => {
|
||||||
|
if $print {
|
||||||
|
println!("[{}] {}", $type, format_args!($($arg)*));
|
||||||
|
}
|
||||||
|
log::error!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version without println and custom message
|
||||||
|
($type:expr, $($arg:tt)*) => {
|
||||||
|
log::error!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pub mod error;
|
|||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
|
pub mod logging;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ impl MihomoManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MihomoManager {
|
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> {
|
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
|
||||||
let url = format!("{}/configs?force=true", self.mihomo_server);
|
let url = format!("{}/configs?force=true", self.mihomo_server);
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.2.1-alpha",
|
"version": "2.2.2",
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@@ -32,8 +32,10 @@
|
|||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||||
|
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||||
|
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
|
||||||
],
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
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 { deleteConnection } from "@/services/api";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
@@ -14,6 +14,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
|||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: (detail: IConnectionsItem) => {
|
open: (detail: IConnectionsItem) => {
|
||||||
@@ -35,6 +36,8 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
|||||||
maxWidth: "520px",
|
maxWidth: "520px",
|
||||||
maxHeight: "480px",
|
maxHeight: "480px",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
message={
|
message={
|
||||||
@@ -54,6 +57,7 @@ interface InnerProps {
|
|||||||
|
|
||||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||||
const { metadata, rulePayload } = data;
|
const { metadata, rulePayload } = data;
|
||||||
|
const theme = useTheme();
|
||||||
const chains = [...data.chains].reverse().join(" / ");
|
const chains = [...data.chains].reverse().join(" / ");
|
||||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||||
const host = metadata.host
|
const host = metadata.host
|
||||||
@@ -99,11 +103,11 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
|||||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ userSelect: "text" }}>
|
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
||||||
{information.map((each) => (
|
{information.map((each) => (
|
||||||
<div key={each.label}>
|
<div key={each.label}>
|
||||||
<b>{each.label}</b>
|
<b>{each.label}</b>
|
||||||
<span style={{ wordBreak: "break-all" }}>: {each.value}</span>
|
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useClash } from "@/hooks/use-clash";
|
|||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { getRules } from "@/services/api";
|
import { getRules } from "@/services/api";
|
||||||
import { getAppUptime } from "@/services/cmds";
|
import { getAppUptime, getSystemProxy } from "@/services/cmds";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
|
||||||
// 将毫秒转换为时:分:秒格式的函数
|
// 将毫秒转换为时:分:秒格式的函数
|
||||||
const formatUptime = (uptimeMs: number) => {
|
const formatUptime = (uptimeMs: number) => {
|
||||||
@@ -21,6 +21,8 @@ export const ClashInfoCard = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clashInfo } = useClashInfo();
|
const { clashInfo } = useClashInfo();
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash();
|
||||||
|
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
|
||||||
|
const [rules, setRules] = useState<any[]>([]);
|
||||||
|
|
||||||
// 使用SWR获取应用运行时间,降低更新频率
|
// 使用SWR获取应用运行时间,降低更新频率
|
||||||
const { data: uptimeMs = 0 } = useSWR(
|
const { data: uptimeMs = 0 } = useSWR(
|
||||||
@@ -33,15 +35,18 @@ export const ClashInfoCard = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 在组件加载时获取系统代理信息和规则数据
|
||||||
|
useEffect(() => {
|
||||||
|
// 获取系统代理信息
|
||||||
|
getSystemProxy().then(setSysproxy);
|
||||||
|
|
||||||
|
// 获取规则数据
|
||||||
|
getRules().then(setRules).catch(() => setRules([]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
||||||
|
|
||||||
// 获取规则数据,只在组件加载时获取一次
|
|
||||||
const { data: rules = [] } = useSWR("getRules", getRules, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
errorRetryCount: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用备忘录组件内容,减少重新渲染
|
// 使用备忘录组件内容,减少重新渲染
|
||||||
const cardContent = useMemo(() => {
|
const cardContent = useMemo(() => {
|
||||||
if (!clashInfo) return null;
|
if (!clashInfo) return null;
|
||||||
@@ -62,7 +67,7 @@ export const ClashInfoCard = () => {
|
|||||||
{t("System Proxy Address")}
|
{t("System Proxy Address")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashInfo.server || "-"}
|
{sysproxy?.server || "-"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -94,7 +99,7 @@ export const ClashInfoCard = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}, [clashInfo, clashVersion, t, uptime, rules.length]);
|
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
MultipleStopRounded,
|
MultipleStopRounded,
|
||||||
DirectionsRounded,
|
DirectionsRounded,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -20,21 +20,19 @@ export const ClashModeCard = () => {
|
|||||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||||
"getClashConfig",
|
"getClashConfig",
|
||||||
getClashConfig,
|
getClashConfig,
|
||||||
{ revalidateOnFocus: false }
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: true,
|
||||||
|
dedupingInterval: 1000,
|
||||||
|
errorRetryInterval: 5000
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||||
|
|
||||||
// 本地状态记录当前模式
|
// 直接使用API返回的模式,不维护本地状态
|
||||||
const [localMode, setLocalMode] = useState<string>("rule");
|
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||||
|
|
||||||
// 当从API获取到当前模式时更新本地状态
|
|
||||||
useEffect(() => {
|
|
||||||
if (clashConfig?.mode) {
|
|
||||||
setLocalMode(clashConfig.mode.toLowerCase());
|
|
||||||
}
|
|
||||||
}, [clashConfig]);
|
|
||||||
|
|
||||||
// 模式图标映射
|
// 模式图标映射
|
||||||
const modeIcons = useMemo(() => ({
|
const modeIcons = useMemo(() => ({
|
||||||
@@ -45,10 +43,7 @@ export const ClashModeCard = () => {
|
|||||||
|
|
||||||
// 切换模式的处理函数
|
// 切换模式的处理函数
|
||||||
const onChangeMode = useLockFn(async (mode: string) => {
|
const onChangeMode = useLockFn(async (mode: string) => {
|
||||||
if (mode === localMode) return;
|
if (mode === currentMode) return;
|
||||||
|
|
||||||
setLocalMode(mode);
|
|
||||||
|
|
||||||
if (verge?.auto_close_connection) {
|
if (verge?.auto_close_connection) {
|
||||||
closeAllConnections();
|
closeAllConnections();
|
||||||
}
|
}
|
||||||
@@ -58,9 +53,6 @@ export const ClashModeCard = () => {
|
|||||||
mutateClash();
|
mutateClash();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to change mode:", error);
|
console.error("Failed to change mode:", error);
|
||||||
if (clashConfig?.mode) {
|
|
||||||
setLocalMode(clashConfig.mode.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,8 +65,8 @@ export const ClashModeCard = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: 1,
|
gap: 1,
|
||||||
bgcolor: mode === localMode ? "primary.main" : "background.paper",
|
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
|
||||||
color: mode === localMode ? "primary.contrastText" : "text.primary",
|
color: mode === currentMode ? "primary.contrastText" : "text.primary",
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
transition: "all 0.2s ease-in-out",
|
transition: "all 0.2s ease-in-out",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@@ -86,7 +78,7 @@ export const ClashModeCard = () => {
|
|||||||
"&:active": {
|
"&:active": {
|
||||||
transform: "translateY(1px)",
|
transform: "translateY(1px)",
|
||||||
},
|
},
|
||||||
"&::after": mode === localMode
|
"&::after": mode === currentMode
|
||||||
? {
|
? {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -132,7 +124,7 @@ export const ClashModeCard = () => {
|
|||||||
{modeList.map((mode) => (
|
{modeList.map((mode) => (
|
||||||
<Paper
|
<Paper
|
||||||
key={mode}
|
key={mode}
|
||||||
elevation={mode === localMode ? 2 : 0}
|
elevation={mode === currentMode ? 2 : 0}
|
||||||
onClick={() => onChangeMode(mode)}
|
onClick={() => onChangeMode(mode)}
|
||||||
sx={buttonStyles(mode)}
|
sx={buttonStyles(mode)}
|
||||||
>
|
>
|
||||||
@@ -141,7 +133,7 @@ export const ClashModeCard = () => {
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: "capitalize",
|
textTransform: "capitalize",
|
||||||
fontWeight: mode === localMode ? 600 : 400,
|
fontWeight: mode === currentMode ? 600 : 400,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t(mode)}
|
{t(mode)}
|
||||||
@@ -161,15 +153,13 @@ export const ClashModeCard = () => {
|
|||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Fade in={true} timeout={200}>
|
<Typography
|
||||||
<Typography
|
variant="caption"
|
||||||
variant="caption"
|
component="div"
|
||||||
component="div"
|
sx={descriptionStyles}
|
||||||
sx={descriptionStyles}
|
>
|
||||||
>
|
{t(`${currentMode} Mode Description`)}
|
||||||
{t(`${localMode} Mode Description`)}
|
</Typography>
|
||||||
</Typography>
|
|
||||||
</Fade>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,11 +53,16 @@ function convertDelayColor(delayValue: number) {
|
|||||||
const mainColor = colorStr.split(".")[0];
|
const mainColor = colorStr.split(".")[0];
|
||||||
|
|
||||||
switch (mainColor) {
|
switch (mainColor) {
|
||||||
case "success": return "success";
|
case "success":
|
||||||
case "warning": return "warning";
|
return "success";
|
||||||
case "error": return "error";
|
case "warning":
|
||||||
case "primary": return "primary";
|
return "warning";
|
||||||
default: return "default";
|
case "error":
|
||||||
|
return "error";
|
||||||
|
case "primary":
|
||||||
|
return "primary";
|
||||||
|
default:
|
||||||
|
return "default";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +84,7 @@ function getSignalIcon(delay: number) {
|
|||||||
// 简单的防抖函数
|
// 简单的防抖函数
|
||||||
function debounce(fn: Function, ms = 100) {
|
function debounce(fn: Function, ms = 100) {
|
||||||
let timeoutId: ReturnType<typeof setTimeout>;
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
return function(this: any, ...args: any[]) {
|
return function (this: any, ...args: any[]) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||||
};
|
};
|
||||||
@@ -87,7 +92,8 @@ function debounce(fn: Function, ms = 100) {
|
|||||||
|
|
||||||
export const CurrentProxyCard = () => {
|
export const CurrentProxyCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentProxy, primaryGroupName, mode, refreshProxy } = useCurrentProxy();
|
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
||||||
|
useCurrentProxy();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
@@ -135,147 +141,150 @@ export const CurrentProxyCard = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 根据模式确定初始组
|
// 根据模式确定初始组
|
||||||
if (isGlobalMode) {
|
if (isGlobalMode) {
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selection: {
|
selection: {
|
||||||
...prev.selection,
|
...prev.selection,
|
||||||
group: "GLOBAL"
|
group: "GLOBAL",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
} else if (isDirectMode) {
|
} else if (isDirectMode) {
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selection: {
|
selection: {
|
||||||
...prev.selection,
|
...prev.selection,
|
||||||
group: "DIRECT"
|
group: "DIRECT",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selection: {
|
selection: {
|
||||||
...prev.selection,
|
...prev.selection,
|
||||||
group: savedGroup || primaryGroupName || ""
|
group: savedGroup || primaryGroupName || "",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
||||||
|
|
||||||
// 带锁的代理数据获取函数,防止并发请求
|
// 带锁的代理数据获取函数,防止并发请求
|
||||||
const fetchProxyData = useCallback(async (force = false) => {
|
const fetchProxyData = useCallback(
|
||||||
// 防止重复请求
|
async (force = false) => {
|
||||||
if (isRefreshingRef.current) {
|
// 防止重复请求
|
||||||
pendingRefreshRef.current = true;
|
if (isRefreshingRef.current) {
|
||||||
return;
|
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]);
|
// 检查刷新间隔
|
||||||
|
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 变化
|
// 响应 currentProxy 变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentProxy && (!state.displayProxy || currentProxy.name !== state.displayProxy.name)) {
|
if (
|
||||||
|
currentProxy &&
|
||||||
|
(!state.displayProxy || currentProxy.name !== state.displayProxy.name)
|
||||||
|
) {
|
||||||
fetchProxyData(true);
|
fetchProxyData(true);
|
||||||
}
|
}
|
||||||
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
||||||
|
|
||||||
// 平滑的定期刷新,使用固定间隔
|
// 监听模式变化,mode变化时刷新
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProxyData();
|
fetchProxyData(true);
|
||||||
|
}, [mode, fetchProxyData]);
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
fetchProxyData();
|
|
||||||
}, 3000); // 使用固定的3秒间隔,平衡响应速度和性能
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [fetchProxyData]);
|
|
||||||
|
|
||||||
// 计算要显示的代理选项 - 使用 useMemo 优化
|
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||||
const proxyOptions = useMemo(() => {
|
const proxyOptions = useMemo(() => {
|
||||||
@@ -285,14 +294,16 @@ export const CurrentProxyCard = () => {
|
|||||||
if (isGlobalMode && state.proxyData.records) {
|
if (isGlobalMode && state.proxyData.records) {
|
||||||
// 全局模式下的选项
|
// 全局模式下的选项
|
||||||
return Object.keys(state.proxyData.records)
|
return Object.keys(state.proxyData.records)
|
||||||
.filter(name => name !== "DIRECT" && name !== "REJECT")
|
.filter((name) => name !== "DIRECT" && name !== "REJECT")
|
||||||
.map(name => ({ name }));
|
.map((name) => ({ name }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通模式
|
// 普通模式
|
||||||
const group = state.proxyData.groups.find(g => g.name === state.selection.group);
|
const group = state.proxyData.groups.find(
|
||||||
|
(g) => g.name === state.selection.group,
|
||||||
|
);
|
||||||
if (group) {
|
if (group) {
|
||||||
return group.all.map(name => ({ name }));
|
return group.all.map((name) => ({ name }));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
||||||
@@ -302,88 +313,103 @@ export const CurrentProxyCard = () => {
|
|||||||
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
||||||
setState(updateFn);
|
setState(updateFn);
|
||||||
}, 50),
|
}, 50),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理代理组变更
|
// 处理代理组变更
|
||||||
const handleGroupChange = useCallback((event: SelectChangeEvent) => {
|
const handleGroupChange = useCallback(
|
||||||
if (isGlobalMode || isDirectMode) return;
|
(event: SelectChangeEvent) => {
|
||||||
|
if (isGlobalMode || isDirectMode) return;
|
||||||
const newGroup = event.target.value;
|
|
||||||
|
const newGroup = event.target.value;
|
||||||
// 保存到本地存储
|
|
||||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
// 保存到本地存储
|
||||||
|
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||||
// 获取该组当前选中的代理
|
|
||||||
setState(prev => {
|
// 获取该组当前选中的代理
|
||||||
const group = prev.proxyData.groups.find(g => g.name === newGroup);
|
setState((prev) => {
|
||||||
if (group) {
|
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 {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
selection: {
|
selection: {
|
||||||
|
...prev.selection,
|
||||||
group: newGroup,
|
group: newGroup,
|
||||||
proxy: group.now
|
|
||||||
},
|
},
|
||||||
displayProxy: prev.proxyData.records[group.now] || null
|
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
return {
|
},
|
||||||
|
[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,
|
...prev,
|
||||||
selection: {
|
selection: {
|
||||||
...prev.selection,
|
...prev.selection,
|
||||||
group: newGroup
|
proxy: newProxy,
|
||||||
}
|
},
|
||||||
};
|
displayProxy: prev.proxyData.records[newProxy] || null,
|
||||||
});
|
}));
|
||||||
}, [isGlobalMode, isDirectMode]);
|
|
||||||
|
|
||||||
// 处理代理节点变更
|
// 非特殊模式下保存到本地存储
|
||||||
const handleProxyChange = useCallback(async (event: SelectChangeEvent) => {
|
if (!isGlobalMode && !isDirectMode) {
|
||||||
if (isDirectMode) return;
|
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新代理信息,使用较短的延迟
|
try {
|
||||||
setTimeout(() => {
|
// 更新代理设置
|
||||||
refreshProxy();
|
await updateProxy(currentGroup, newProxy);
|
||||||
fetchProxyData(true);
|
|
||||||
}, 200);
|
// 自动关闭连接设置
|
||||||
} catch (error) {
|
if (verge?.auto_close_connection && previousProxy) {
|
||||||
console.error("更新代理失败", error);
|
getConnections().then(({ connections }) => {
|
||||||
}
|
connections.forEach((conn) => {
|
||||||
}, [isDirectMode, isGlobalMode, state.proxyData.records, state.selection, verge?.auto_close_connection, refreshProxy, fetchProxyData, debouncedSetState]);
|
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(() => {
|
const goToProxies = useCallback(() => {
|
||||||
@@ -392,35 +418,38 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 获取要显示的代理节点
|
// 获取要显示的代理节点
|
||||||
const proxyToDisplay = state.displayProxy || currentProxy;
|
const proxyToDisplay = state.displayProxy || currentProxy;
|
||||||
|
|
||||||
// 获取当前节点的延迟
|
// 获取当前节点的延迟
|
||||||
const currentDelay = proxyToDisplay
|
const currentDelay = proxyToDisplay
|
||||||
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
// 获取信号图标
|
// 获取信号图标
|
||||||
const signalInfo = getSignalIcon(currentDelay);
|
const signalInfo = getSignalIcon(currentDelay);
|
||||||
|
|
||||||
// 自定义渲染选择框中的值
|
// 自定义渲染选择框中的值
|
||||||
const renderProxyValue = useCallback((selected: string) => {
|
const renderProxyValue = useCallback(
|
||||||
if (!selected || !state.proxyData.records[selected]) return selected;
|
(selected: string) => {
|
||||||
|
if (!selected || !state.proxyData.records[selected]) return selected;
|
||||||
|
|
||||||
const delayValue = delayManager.getDelayFix(
|
const delayValue = delayManager.getDelayFix(
|
||||||
state.proxyData.records[selected],
|
state.proxyData.records[selected],
|
||||||
state.selection.group
|
state.selection.group,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<Typography noWrap>{selected}</Typography>
|
<Typography noWrap>{selected}</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={delayManager.formatDelay(delayValue)}
|
label={delayManager.formatDelay(delayValue)}
|
||||||
color={convertDelayColor(delayValue)}
|
color={convertDelayColor(delayValue)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}, [state.proxyData.records, state.selection.group]);
|
},
|
||||||
|
[state.proxyData.records, state.selection.group],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
@@ -471,22 +500,48 @@ export const CurrentProxyCard = () => {
|
|||||||
{proxyToDisplay.name}
|
{proxyToDisplay.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
<Box
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
{proxyToDisplay.type}
|
{proxyToDisplay.type}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isGlobalMode && (
|
{isGlobalMode && (
|
||||||
<Chip size="small" label={t("Global Mode")} color="primary" sx={{ mr: 0.5 }} />
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={t("Global Mode")}
|
||||||
|
color="primary"
|
||||||
|
sx={{ mr: 0.5 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{isDirectMode && (
|
{isDirectMode && (
|
||||||
<Chip size="small" label={t("Direct Mode")} color="success" sx={{ mr: 0.5 }} />
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={t("Direct Mode")}
|
||||||
|
color="success"
|
||||||
|
sx={{ mr: 0.5 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{/* 节点特性 */}
|
{/* 节点特性 */}
|
||||||
{proxyToDisplay.udp && <Chip size="small" label="UDP" variant="outlined" />}
|
{proxyToDisplay.udp && (
|
||||||
{proxyToDisplay.tfo && <Chip size="small" label="TFO" variant="outlined" />}
|
<Chip size="small" label="UDP" variant="outlined" />
|
||||||
{proxyToDisplay.xudp && <Chip size="small" label="XUDP" variant="outlined" />}
|
)}
|
||||||
{proxyToDisplay.mptcp && <Chip size="small" label="MPTCP" variant="outlined" />}
|
{proxyToDisplay.tfo && (
|
||||||
{proxyToDisplay.smux && <Chip size="small" label="SMUX" variant="outlined" />}
|
<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>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -500,7 +555,12 @@ export const CurrentProxyCard = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{/* 代理组选择器 */}
|
{/* 代理组选择器 */}
|
||||||
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 1.5 }}>
|
<FormControl
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
sx={{ mb: 1.5 }}
|
||||||
|
>
|
||||||
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
|
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="proxy-group-select-label"
|
labelId="proxy-group-select-label"
|
||||||
@@ -535,39 +595,41 @@ export const CurrentProxyCard = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{proxyOptions.map((proxy) => {
|
{isDirectMode
|
||||||
const delayValue = delayManager.getDelayFix(
|
? null
|
||||||
state.proxyData.records[proxy.name],
|
: proxyOptions.map((proxy) => {
|
||||||
state.selection.group
|
const delayValue = delayManager.getDelayFix(
|
||||||
);
|
state.proxyData.records[proxy.name],
|
||||||
return (
|
state.selection.group,
|
||||||
<MenuItem
|
);
|
||||||
key={proxy.name}
|
return (
|
||||||
value={proxy.name}
|
<MenuItem
|
||||||
sx={{
|
key={proxy.name}
|
||||||
display: "flex",
|
value={proxy.name}
|
||||||
justifyContent: "space-between",
|
sx={{
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
width: "100%",
|
justifyContent: "space-between",
|
||||||
pr: 1,
|
alignItems: "center",
|
||||||
}}
|
width: "100%",
|
||||||
>
|
pr: 1,
|
||||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
}}
|
||||||
{proxy.name}
|
>
|
||||||
</Typography>
|
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||||
<Chip
|
{proxy.name}
|
||||||
size="small"
|
</Typography>
|
||||||
label={delayManager.formatDelay(delayValue)}
|
<Chip
|
||||||
color={convertDelayColor(delayValue)}
|
size="small"
|
||||||
sx={{
|
label={delayManager.formatDelay(delayValue)}
|
||||||
minWidth: "60px",
|
color={convertDelayColor(delayValue)}
|
||||||
height: "22px",
|
sx={{
|
||||||
flexShrink: 0,
|
minWidth: "60px",
|
||||||
}}
|
height: "22px",
|
||||||
/>
|
flexShrink: 0,
|
||||||
</MenuItem>
|
}}
|
||||||
);
|
/>
|
||||||
})}
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ export const EnhancedCard = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
|
|
||||||
|
// 统一的标题截断样式
|
||||||
|
const titleTruncateStyle = {
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
display: "block"
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -52,7 +62,13 @@ export const EnhancedCard = ({
|
|||||||
borderColor: "divider",
|
borderColor: "divider",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
minWidth: 0,
|
||||||
|
flex: 1,
|
||||||
|
overflow: "hidden"
|
||||||
|
}}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -62,21 +78,32 @@ export const EnhancedCard = ({
|
|||||||
width: 38,
|
width: 38,
|
||||||
height: 38,
|
height: 38,
|
||||||
mr: 1.5,
|
mr: 1.5,
|
||||||
|
flexShrink: 0,
|
||||||
backgroundColor: alpha(theme.palette[iconColor].main, 0.12),
|
backgroundColor: alpha(theme.palette[iconColor].main, 0.12),
|
||||||
color: theme.palette[iconColor].main,
|
color: theme.palette[iconColor].main,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Box>
|
</Box>
|
||||||
{typeof title === "string" ? (
|
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||||
<Typography variant="h6" fontWeight="medium" fontSize={18}>
|
{typeof title === "string" ? (
|
||||||
{title}
|
<Typography
|
||||||
</Typography>
|
variant="h6"
|
||||||
) : (
|
fontWeight="medium"
|
||||||
title
|
fontSize={18}
|
||||||
)}
|
sx={titleTruncateStyle}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Box sx={titleTruncateStyle}>
|
||||||
|
{title}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{action}
|
{action && <Box sx={{ ml: 2, flexShrink: 0 }}>{action}</Box>}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -35,19 +35,10 @@ const round = keyframes`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// 辅助函数解析URL和过期时间
|
// 辅助函数解析URL和过期时间
|
||||||
const parseUrl = (url?: string, maxLength: number = 25) => {
|
const parseUrl = (url?: string) => {
|
||||||
if (!url) return "-";
|
if (!url) return "-";
|
||||||
let parsedUrl = "";
|
if (url.startsWith("http")) return new URL(url).host;
|
||||||
if (url.startsWith("http")) {
|
return "local";
|
||||||
parsedUrl = new URL(url).host;
|
|
||||||
} else {
|
|
||||||
parsedUrl = "local";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedUrl.length > maxLength) {
|
|
||||||
return parsedUrl.substring(0, maxLength - 3) + "...";
|
|
||||||
}
|
|
||||||
return parsedUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseExpire = (expire?: number) => {
|
const parseExpire = (expire?: number) => {
|
||||||
@@ -81,6 +72,14 @@ export interface HomeProfileCardProps {
|
|||||||
onProfileUpdated?: () => void;
|
onProfileUpdated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加一个通用的截断样式
|
||||||
|
const truncateStyle = {
|
||||||
|
maxWidth: "calc(100% - 28px)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
};
|
||||||
|
|
||||||
// 提取独立组件减少主组件复杂度
|
// 提取独立组件减少主组件复杂度
|
||||||
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||||
current: ProfileItem;
|
current: ProfileItem;
|
||||||
@@ -109,31 +108,55 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
|||||||
{current.url && (
|
{current.url && (
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<DnsOutlined fontSize="small" color="action" />
|
<DnsOutlined fontSize="small" color="action" />
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}>
|
||||||
{t("From")}:{" "}
|
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
|
||||||
{current.home ? (
|
{current.home ? (
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
onClick={() => current.home && openWebUrl(current.home)}
|
onClick={() => current.home && openWebUrl(current.home)}
|
||||||
sx={{
|
sx={{
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center"
|
alignItems: "center",
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: "calc(100% - 40px)",
|
||||||
|
ml: 0.5
|
||||||
}}
|
}}
|
||||||
|
title={parseUrl(current.url)}
|
||||||
>
|
>
|
||||||
{parseUrl(current.url)}
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
minWidth: 0,
|
||||||
|
flex: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{parseUrl(current.url)}
|
||||||
|
</Typography>
|
||||||
<LaunchOutlined
|
<LaunchOutlined
|
||||||
fontSize="inherit"
|
fontSize="inherit"
|
||||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
|
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Typography
|
||||||
component="span"
|
component="span"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
minWidth: 0,
|
||||||
|
flex: 1,
|
||||||
|
ml: 0.5
|
||||||
|
}}
|
||||||
|
title={parseUrl(current.url)}
|
||||||
>
|
>
|
||||||
{parseUrl(current.url)}
|
{parseUrl(current.url)}
|
||||||
</Box>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -285,16 +308,30 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
|||||||
fontSize={18}
|
fontSize={18}
|
||||||
onClick={() => current.home && openWebUrl(current.home)}
|
onClick={() => current.home && openWebUrl(current.home)}
|
||||||
sx={{
|
sx={{
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: "100%",
|
||||||
|
"& > span": {
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
title={current.name}
|
||||||
>
|
>
|
||||||
{current.name}
|
<span>{current.name}</span>
|
||||||
<LaunchOutlined
|
<LaunchOutlined
|
||||||
fontSize="inherit"
|
fontSize="inherit"
|
||||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
sx={{
|
||||||
|
ml: 0.5,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
opacity: 0.7,
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export const IpInfoCard = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
<Skeleton variant="text" width="60%" height={32} />
|
<Skeleton variant="text" width="60%" height={30} />
|
||||||
<Skeleton variant="text" width="80%" height={24} />
|
<Skeleton variant="text" width="80%" height={24} />
|
||||||
<Skeleton variant="text" width="70%" height={24} />
|
<Skeleton variant="text" width="70%" height={24} />
|
||||||
<Skeleton variant="text" width="50%" height={24} />
|
<Skeleton variant="text" width="50%" height={24} />
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
getAutotemProxy,
|
getAutotemProxy,
|
||||||
getRunningMode,
|
getRunningMode,
|
||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
||||||
|
|
||||||
@@ -151,6 +152,10 @@ export const ProxyTunCard: FC = () => {
|
|||||||
// 获取代理状态信息
|
// 获取代理状态信息
|
||||||
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
||||||
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
||||||
|
const { verge } = useVerge();
|
||||||
|
|
||||||
|
// 从verge配置中获取开关状态
|
||||||
|
const { enable_system_proxy, enable_tun_mode } = verge ?? {};
|
||||||
|
|
||||||
// 是否以sidecar模式运行
|
// 是否以sidecar模式运行
|
||||||
const isSidecarMode = runningMode === "sidecar";
|
const isSidecarMode = runningMode === "sidecar";
|
||||||
@@ -170,7 +175,7 @@ export const ProxyTunCard: FC = () => {
|
|||||||
const tabDescription = useMemo(() => {
|
const tabDescription = useMemo(() => {
|
||||||
if (activeTab === "system") {
|
if (activeTab === "system") {
|
||||||
return {
|
return {
|
||||||
text: sysproxy?.enable
|
text: enable_system_proxy
|
||||||
? t("System Proxy Enabled")
|
? t("System Proxy Enabled")
|
||||||
: t("System Proxy Disabled"),
|
: t("System Proxy Disabled"),
|
||||||
tooltip: t("System Proxy Info")
|
tooltip: t("System Proxy Info")
|
||||||
@@ -179,11 +184,13 @@ export const ProxyTunCard: FC = () => {
|
|||||||
return {
|
return {
|
||||||
text: isSidecarMode
|
text: isSidecarMode
|
||||||
? t("TUN Mode Service Required")
|
? t("TUN Mode Service Required")
|
||||||
: t("TUN Mode Intercept Info"),
|
: enable_tun_mode
|
||||||
tooltip: t("Tun Mode Info")
|
? t("TUN Mode Enabled")
|
||||||
|
: t("TUN Mode Disabled"),
|
||||||
|
tooltip: t("TUN Mode Intercept Info")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [activeTab, sysproxy?.enable, isSidecarMode, t]);
|
}, [activeTab, enable_system_proxy, enable_tun_mode, isSidecarMode, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||||
@@ -203,13 +210,14 @@ export const ProxyTunCard: FC = () => {
|
|||||||
onClick={() => handleTabChange("system")}
|
onClick={() => handleTabChange("system")}
|
||||||
icon={ComputerRounded}
|
icon={ComputerRounded}
|
||||||
label={t("System Proxy")}
|
label={t("System Proxy")}
|
||||||
hasIndicator={sysproxy?.enable}
|
hasIndicator={enable_system_proxy}
|
||||||
/>
|
/>
|
||||||
<TabButton
|
<TabButton
|
||||||
isActive={activeTab === "tun"}
|
isActive={activeTab === "tun"}
|
||||||
onClick={() => handleTabChange("tun")}
|
onClick={() => handleTabChange("tun")}
|
||||||
icon={TroubleshootRounded}
|
icon={TroubleshootRounded}
|
||||||
label={t("Tun Mode")}
|
label={t("Tun Mode")}
|
||||||
|
hasIndicator={enable_tun_mode && !isSidecarMode}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const HOTKEY_FUNC = [
|
|||||||
"clash_mode_direct",
|
"clash_mode_direct",
|
||||||
"toggle_system_proxy",
|
"toggle_system_proxy",
|
||||||
"toggle_tun_mode",
|
"toggle_tun_mode",
|
||||||
|
"entry_lightweight_mode",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||||
|
|||||||
@@ -198,6 +198,26 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</GuardState>
|
</GuardState>
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
|
{OS === "macos" && (
|
||||||
|
<Item>
|
||||||
|
<ListItemText primary={t("Enable Tray Icon")} />
|
||||||
|
<GuardState
|
||||||
|
value={
|
||||||
|
verge?.enable_tray_icon === false &&
|
||||||
|
verge?.enable_tray_speed === false
|
||||||
|
? true
|
||||||
|
: (verge?.enable_tray_icon ?? true)
|
||||||
|
}
|
||||||
|
valueProps="checked"
|
||||||
|
onCatch={onError}
|
||||||
|
onFormat={onSwitchFormat}
|
||||||
|
onChange={(e) => onChangeData({ enable_tray_icon: e })}
|
||||||
|
onGuard={(e) => patchVerge({ enable_tray_icon: e })}
|
||||||
|
>
|
||||||
|
<Switch edge="end" />
|
||||||
|
</GuardState>
|
||||||
|
</Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<ListItemText primary={t("Common Tray Icon")} />
|
<ListItemText primary={t("Common Tray Icon")} />
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: "primary.main",
|
color: "primary.main",
|
||||||
"&:hover": { textDecoration: "underline" }
|
"&:hover": { textDecoration: "underline" },
|
||||||
}}
|
}}
|
||||||
onClick={() => entry_lightweight_mode()}
|
onClick={async () => await entry_lightweight_mode()}
|
||||||
>
|
>
|
||||||
{t("Enable")}
|
{t("Enable")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -115,17 +115,25 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">{t("mins")}</InputAdornment>
|
<InputAdornment position="end">
|
||||||
)
|
{t("mins")}
|
||||||
}
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem sx={{ padding: "5px 2px" }}>
|
<ListItem sx={{ padding: "5px 2px" }}>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: "italic" }}>
|
<Typography
|
||||||
{t("When closing the window, LightWeight Mode will be automatically activated after _n minutes",
|
variant="body2"
|
||||||
{ n: values.autoEnterLiteModeDelay })}
|
color="text.secondary"
|
||||||
|
sx={{ fontStyle: "italic" }}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
|
||||||
|
{ n: values.autoEnterLiteModeDelay },
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
@@ -133,4 +141,4 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
</List>
|
</List>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -120,17 +120,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
{proxy_auto_config ? (
|
{enable_system_proxy ? (
|
||||||
autoproxy?.enable ? (
|
|
||||||
<PlayCircleOutlineRounded
|
|
||||||
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PauseCircleOutlineRounded
|
|
||||||
sx={{ color: "text.disabled", mr: 1.5, fontSize: 28 }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : sysproxy?.enable ? (
|
|
||||||
<PlayCircleOutlineRounded
|
<PlayCircleOutlineRounded
|
||||||
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+5
-2
@@ -205,8 +205,10 @@
|
|||||||
"Tun Mode Info": "Tun (Virtual NIC) mode: Captures all system traffic, when enabled, there is no need to enable system proxy.",
|
"Tun Mode Info": "Tun (Virtual NIC) mode: Captures all system traffic, when enabled, there is no need to enable system proxy.",
|
||||||
"System Proxy Enabled": "System proxy is enabled, your applications will access the network through the proxy",
|
"System Proxy Enabled": "System proxy is enabled, your applications will access the network through the proxy",
|
||||||
"System Proxy Disabled": "System proxy is disabled, it is recommended for most users to turn on this option",
|
"System Proxy Disabled": "System proxy is disabled, it is recommended for most users to turn on this option",
|
||||||
|
"TUN Mode Enabled": "TUN mode is enabled, applications will access the network through the virtual network card",
|
||||||
|
"TUN Mode Disabled": "TUN mode is disabled, suitable for special applications",
|
||||||
"TUN Mode Service Required": "TUN mode requires service mode, please install the service first",
|
"TUN Mode Service Required": "TUN mode requires service mode, please install the service first",
|
||||||
"TUN Mode Intercept Info": "TUN mode can take over all application traffic, suitable for special applications",
|
"TUN Mode Intercept Info": "TUN mode can take over all application traffic, suitable for special applications that do not follow the system proxy settings",
|
||||||
"rule Mode Description": "Routes traffic according to preset rules, provides flexible proxy strategies",
|
"rule Mode Description": "Routes traffic according to preset rules, provides flexible proxy strategies",
|
||||||
"global Mode Description": "All traffic goes through proxy servers, suitable for scenarios requiring global internet access",
|
"global Mode Description": "All traffic goes through proxy servers, suitable for scenarios requiring global internet access",
|
||||||
"direct Mode Description": "All traffic doesn't go through proxy nodes, but is forwarded by Clash kernel to target servers, suitable for specific scenarios requiring kernel traffic distribution",
|
"direct Mode Description": "All traffic doesn't go through proxy nodes, but is forwarded by Clash kernel to target servers, suitable for specific scenarios requiring kernel traffic distribution",
|
||||||
@@ -351,6 +353,7 @@
|
|||||||
"clash_mode_direct": "Direct Mode",
|
"clash_mode_direct": "Direct Mode",
|
||||||
"toggle_system_proxy": "Enable/Disable System Proxy",
|
"toggle_system_proxy": "Enable/Disable System Proxy",
|
||||||
"toggle_tun_mode": "Enable/Disable Tun Mode",
|
"toggle_tun_mode": "Enable/Disable Tun Mode",
|
||||||
|
"entry_lightweight_mode": "Entry Lightweight Mode",
|
||||||
"Backup Setting": "Backup Setting",
|
"Backup Setting": "Backup Setting",
|
||||||
"Backup Setting Info": "Support WebDAV backup configuration files",
|
"Backup Setting Info": "Support WebDAV backup configuration files",
|
||||||
"Runtime Config": "Runtime Config",
|
"Runtime Config": "Runtime Config",
|
||||||
@@ -451,6 +454,7 @@
|
|||||||
"Global Mode": "Global Mode",
|
"Global Mode": "Global Mode",
|
||||||
"Direct Mode": "Direct Mode",
|
"Direct Mode": "Direct Mode",
|
||||||
"Enable Tray Speed": "Enable Tray Speed",
|
"Enable Tray Speed": "Enable Tray Speed",
|
||||||
|
"Enable Tray Icon": "Enable Tray Icon",
|
||||||
"LightWeight Mode": "Lightweight Mode",
|
"LightWeight Mode": "Lightweight Mode",
|
||||||
"LightWeight Mode Info": "Close the GUI and keep only the kernel running",
|
"LightWeight Mode Info": "Close the GUI and keep only the kernel running",
|
||||||
"LightWeight Mode Settings": "LightWeight Mode Settings",
|
"LightWeight Mode Settings": "LightWeight Mode Settings",
|
||||||
@@ -570,7 +574,6 @@
|
|||||||
"No": "No",
|
"No": "No",
|
||||||
"Failed": "Failed",
|
"Failed": "Failed",
|
||||||
"Completed": "Completed",
|
"Completed": "Completed",
|
||||||
"Bahamut Anime": "Bahamut Anime",
|
|
||||||
"Disallowed ISP": "Disallowed ISP",
|
"Disallowed ISP": "Disallowed ISP",
|
||||||
"Originals Only": "Originals Only",
|
"Originals Only": "Originals Only",
|
||||||
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
|
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
|
||||||
|
|||||||
+217
-89
@@ -16,12 +16,13 @@
|
|||||||
"Delete": "Удалить",
|
"Delete": "Удалить",
|
||||||
"Enable": "Включить",
|
"Enable": "Включить",
|
||||||
"Disable": "Отключить",
|
"Disable": "Отключить",
|
||||||
|
"Label-Home": "Главная",
|
||||||
"Label-Proxies": "Прокси",
|
"Label-Proxies": "Прокси",
|
||||||
"Label-Profiles": "Профили",
|
"Label-Profiles": "Профили",
|
||||||
"Label-Connections": "Соединения",
|
"Label-Connections": "Соединения",
|
||||||
"Label-Rules": "Правила",
|
"Label-Rules": "Правила",
|
||||||
"Label-Logs": "Логи",
|
"Label-Logs": "Логи",
|
||||||
"Label-Test": "Тест",
|
"Label-Unlock": "Тест",
|
||||||
"Label-Settings": "Настройки",
|
"Label-Settings": "Настройки",
|
||||||
"Proxies": "Прокси",
|
"Proxies": "Прокси",
|
||||||
"Proxy Groups": "Группы прокси",
|
"Proxy Groups": "Группы прокси",
|
||||||
@@ -39,12 +40,12 @@
|
|||||||
"Sort by name": "Сортировать по названию",
|
"Sort by name": "Сортировать по названию",
|
||||||
"Delay check URL": "URL проверки задержки",
|
"Delay check URL": "URL проверки задержки",
|
||||||
"Delay check to cancel fixed": "Проверка задержки для отмены фиксированного",
|
"Delay check to cancel fixed": "Проверка задержки для отмены фиксированного",
|
||||||
"Proxy basic": "Резюме о прокси",
|
"Proxy basic": "Отображать меньше сведений о прокси",
|
||||||
"Proxy detail": "Подробности о прокси",
|
"Proxy detail": "Отображать больше сведений о прокси",
|
||||||
"Profiles": "Профили",
|
"Profiles": "Профили",
|
||||||
"Update All Profiles": "Обновить все профили",
|
"Update All Profiles": "Обновить все профили",
|
||||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||||
"Reactivate Profiles": "Реактивировать профили",
|
"Reactivate Profiles": "Перезапустить профиль",
|
||||||
"Paste": "Вставить",
|
"Paste": "Вставить",
|
||||||
"Profile URL": "URL профиля",
|
"Profile URL": "URL профиля",
|
||||||
"Import": "Импорт",
|
"Import": "Импорт",
|
||||||
@@ -135,19 +136,19 @@
|
|||||||
"Hidden": "Скрытый",
|
"Hidden": "Скрытый",
|
||||||
"Group Name Required": "Требуется имя группы",
|
"Group Name Required": "Требуется имя группы",
|
||||||
"Group Name Already Exists": "Имя группы уже существует",
|
"Group Name Already Exists": "Имя группы уже существует",
|
||||||
"Extend Config": "Изменить Merge.",
|
"Extend Config": "Изменить Merge",
|
||||||
"Extend Script": "Изменить Script",
|
"Extend Script": "Изменить Script",
|
||||||
"Global Merge": "Глобальный расширенный Настройки",
|
"Global Merge": "Глобальный расширенный Настройки",
|
||||||
"Global Script": "Глобальный расширенный скрипт",
|
"Global Script": "Глобальный расширенный скрипт",
|
||||||
"Type": "Тип",
|
"Type": "Тип",
|
||||||
"Name": "Название",
|
"Name": "Название",
|
||||||
"Descriptions": "Описания",
|
"Descriptions": "Описание",
|
||||||
"Subscription URL": "URL подписки",
|
"Subscription URL": "URL подписки",
|
||||||
"Update Interval": "Интервал обновления",
|
"Update Interval": "Интервал обновления",
|
||||||
"Choose File": "Выбрать файл",
|
"Choose File": "Выбрать файл",
|
||||||
"Use System Proxy": "Использовать системный прокси для обновления",
|
"Use System Proxy": "Использовать системный прокси для обновления",
|
||||||
"Use Clash Proxy": "Использовать прокси Clash для обновления",
|
"Use Clash Proxy": "Использовать прокси Clash для обновления",
|
||||||
"Accept Invalid Certs (Danger)": "Принимать недействительные сертификаты (Опасно)",
|
"Accept Invalid Certs (Danger)": "Принимать недействительные сертификаты (ОПАСНО)",
|
||||||
"Refresh": "Обновить",
|
"Refresh": "Обновить",
|
||||||
"Home": "Главная",
|
"Home": "Главная",
|
||||||
"Select": "Выбрать",
|
"Select": "Выбрать",
|
||||||
@@ -162,17 +163,19 @@
|
|||||||
"To Top": "Наверх",
|
"To Top": "Наверх",
|
||||||
"To End": "Вниз",
|
"To End": "Вниз",
|
||||||
"Connections": "Соединения",
|
"Connections": "Соединения",
|
||||||
"Table View": "Tablichnyy vid",
|
"Table View": "Отображать в виде таблицы",
|
||||||
"List View": "Spiskovyy vid",
|
"List View": "Отображать в виде списка",
|
||||||
"Close All": "Закрыть всё",
|
"Close All": "Закрыть всё",
|
||||||
"Default": "По умолчанию",
|
"Upload": "Загрузка",
|
||||||
"Download Speed": "Скорость загрузки",
|
"Download": "Скачивание",
|
||||||
|
"Download Speed": "Скорость скачивания",
|
||||||
"Upload Speed": "Скорость загрузки",
|
"Upload Speed": "Скорость загрузки",
|
||||||
"Host": "Хост",
|
"Host": "Хост",
|
||||||
"Downloaded": "Скачано",
|
"Downloaded": "Скачано",
|
||||||
"Uploaded": "Загружено",
|
"Uploaded": "Загружено",
|
||||||
"DL Speed": "Скорость загрузки",
|
"DL Speed": "Скорость скачивания",
|
||||||
"UL Speed": "Скорость выгрузки",
|
"UL Speed": "Скорость загрузки",
|
||||||
|
"Active Connections": "Активные соединения",
|
||||||
"Chains": "Цепочки",
|
"Chains": "Цепочки",
|
||||||
"Rule": "Правило",
|
"Rule": "Правило",
|
||||||
"Process": "Процесс",
|
"Process": "Процесс",
|
||||||
@@ -182,31 +185,43 @@
|
|||||||
"DestinationPort": "Целевой порт",
|
"DestinationPort": "Целевой порт",
|
||||||
"Close Connection": "Закрыть соединение",
|
"Close Connection": "Закрыть соединение",
|
||||||
"Rules": "Правила",
|
"Rules": "Правила",
|
||||||
"Rule Provider": "Провайдер правило",
|
"Rule Provider": "Провайдеры правил",
|
||||||
"Logs": "Логи",
|
"Logs": "Логи",
|
||||||
"Pause": "Пауза",
|
"Pause": "Пауза",
|
||||||
"Clear": "Очистить",
|
"Clear": "Очистить",
|
||||||
"Test": "Тест",
|
"Test": "Тест",
|
||||||
"Test All": "Тест Все",
|
"Test All": "Тестировать все",
|
||||||
|
"Testing...": "Тестирование ...",
|
||||||
"Create Test": "Создать тест",
|
"Create Test": "Создать тест",
|
||||||
"Edit Test": "Редактировать тест",
|
"Edit Test": "Редактировать тест",
|
||||||
"Icon": "Икона",
|
"Icon": "Иконка",
|
||||||
"Test URL": "Тестовый URL",
|
"Test URL": "URL проверка",
|
||||||
"Settings": "Настройки",
|
"Settings": "Настройки",
|
||||||
"System Setting": "Настройки системы",
|
"System Setting": "Настройки системы",
|
||||||
"Tun Mode": "Tun (виртуальный сетевой адаптер) режим",
|
"Tun Mode": "Режим TUN",
|
||||||
"Reset to Default": "Сбросить настройки по умолчанию",
|
"TUN requires Service Mode": "Режим TUN требует установленную службу Clash Verge",
|
||||||
"Tun Mode Info": "Режим Tun (виртуальный сетевой адаптер): захватывает весь системный трафик, при включении нет необходимости включать системный прокси-сервер.",
|
"Install Service": "Установить службу",
|
||||||
|
"Reset to Default": "Сбросить настройки",
|
||||||
|
"Tun Mode Info": "Режим Tun: захватывает весь системный трафик, при включении нет необходимости включать системный прокси-сервер.",
|
||||||
|
"System Proxy Enabled": "Системный прокси включен, ваши приложения будут получать доступ к сети через него",
|
||||||
|
"System Proxy Disabled": "Системный прокси отключен, большинству пользователей рекомендуется включить эту опцию",
|
||||||
|
"TUN Mode Enabled": "Режим TUN включен, приложения будут получать доступ к сети через виртуальную сетевую карту",
|
||||||
|
"TUN Mode Disabled": "Режим TUN отключен",
|
||||||
|
"TUN Mode Service Required": "Режим TUN требует установленную службу Clash Verge",
|
||||||
|
"TUN Mode Intercept Info": "Режим TUN может перехватить трафик всех приложений, подходит для приложений, которые не работают в режиме системного прокси.",
|
||||||
|
"rule Mode Description": "Направляет трафик в соответствии с предустановленными правилами",
|
||||||
|
"global Mode Description": "Направляет весь трафик через прокси-серверы",
|
||||||
|
"direct Mode Description": "Весь трафик обходит прокси, но передается ядром Clash для целевых серверов, подходит для конкретных сценариев, требующих распределения трафика ядра",
|
||||||
"Stack": "Стек",
|
"Stack": "Стек",
|
||||||
"System and Mixed Can Only be Used in Service Mode": "Система и смешанные могут использоваться только в сервисном режиме",
|
"System and Mixed Can Only be Used in Service Mode": "Стэк System и Mixed могут использоваться только в режиме системной службы",
|
||||||
"Device": "Имя устройства",
|
"Device": "Имя устройства",
|
||||||
"Auto Route": "Автоматическая маршрутизация",
|
"Auto Route": "Автоматическая маршрутизация",
|
||||||
"Strict Route": "Строгий маршрут",
|
"Strict Route": "Строгая маршрутизация",
|
||||||
"Auto Detect Interface": "Автоопределение интерфейса",
|
"Auto Detect Interface": "Автоопределение интерфейса",
|
||||||
"DNS Hijack": "DNS-перехват",
|
"DNS Hijack": "DNS-перехват",
|
||||||
"MTU": "Максимальная единица передачи",
|
"MTU": "MTU",
|
||||||
"Service Mode": "Режим сервиса",
|
"Service Mode": "Режим системной службы",
|
||||||
"Service Mode Info": "Установите сервисный режим перед включением режима TUN. Процесс ядра, запущенный службой, может получить разрешение на установку виртуальной сетевой карты (режим TUN).",
|
"Service Mode Info": "Установите режим системной службы перед включением режима TUN. Процесс ядра, запущенный службой, может получить разрешение на установку виртуальной сетевой карты (режим TUN).",
|
||||||
"Current State": "Текущее состояние",
|
"Current State": "Текущее состояние",
|
||||||
"pending": "Ожидающий",
|
"pending": "Ожидающий",
|
||||||
"installed": "Установленный",
|
"installed": "Установленный",
|
||||||
@@ -216,7 +231,7 @@
|
|||||||
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен",
|
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен",
|
||||||
"Install": "Установить",
|
"Install": "Установить",
|
||||||
"Uninstall": "Удалить",
|
"Uninstall": "Удалить",
|
||||||
"Disable Service Mode": "Отключить режим обслуживания",
|
"Disable Service Mode": "Отключить режим системной службы",
|
||||||
"System Proxy": "Системный прокси",
|
"System Proxy": "Системный прокси",
|
||||||
"System Proxy Info": "Разрешить изменение настроек прокси-сервера операционной системы. Если разрешение не удастся, измените настройки прокси-сервера операционной системы вручную",
|
"System Proxy Info": "Разрешить изменение настроек прокси-сервера операционной системы. Если разрешение не удастся, измените настройки прокси-сервера операционной системы вручную",
|
||||||
"System Proxy Setting": "Настройка системного прокси",
|
"System Proxy Setting": "Настройка системного прокси",
|
||||||
@@ -226,30 +241,30 @@
|
|||||||
"Disabled": "Отключено",
|
"Disabled": "Отключено",
|
||||||
"Server Addr": "Адрес сервера: ",
|
"Server Addr": "Адрес сервера: ",
|
||||||
"Not available": "Недоступно",
|
"Not available": "Недоступно",
|
||||||
"Proxy Guard": "Защита прокси",
|
"Proxy Guard": "Proxy Guard",
|
||||||
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим программным обеспечением",
|
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим ПО",
|
||||||
"Guard Duration": "Период защиты",
|
"Guard Duration": "Период защиты",
|
||||||
"Always use Default Bypass": "Всегда использовать стандартное обходное решение",
|
"Always use Default Bypass": "Всегда использовать стандартное обходное решение",
|
||||||
"Use Bypass Check": "Используйте проверку обхода",
|
"Use Bypass Check": "Используйте проверку обхода",
|
||||||
"Proxy Bypass": "Игнорирование прокси: ",
|
"Proxy Bypass": "Игнорируемые адреса: ",
|
||||||
"Bypass": "Игнорирование: ",
|
"Bypass": "Игнорируемые адреса: ",
|
||||||
"Use PAC Mode": "Используйте режим PAC",
|
"Use PAC Mode": "Используйте режим PAC",
|
||||||
"PAC Script Content": "Содержание сценария PAC",
|
"PAC Script Content": "Содержание сценария PAC",
|
||||||
"PAC URL": "Адрес PAC: ",
|
"PAC URL": "Адрес PAC: ",
|
||||||
"Auto Launch": "Автозапуск",
|
"Auto Launch": "Автозапуск",
|
||||||
"Silent Start": "Тихий запуск",
|
"Silent Start": "Тихий запуск",
|
||||||
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
|
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
|
||||||
"TG Channel": "Канал Telegram",
|
"TG Channel": "Telegram-канал",
|
||||||
"Manual": "Документация",
|
"Manual": "Документация",
|
||||||
"Github Repo": "GitHub репозиторий",
|
"Github Repo": "GitHub репозиторий",
|
||||||
"Clash Setting": "Настройки Clash",
|
"Clash Setting": "Настройки Clash",
|
||||||
"Allow Lan": "Разрешить локальную сеть",
|
"Allow Lan": "Разрешить доступ из локальной сети",
|
||||||
"Network Interface": "Сетевой интерфейс",
|
"Network Interface": "Сетевой интерфейс",
|
||||||
"Ip Address": "IP адрес",
|
"Ip Address": "IP адрес",
|
||||||
"Mac Address": "MAC адрес",
|
"Mac Address": "MAC адрес",
|
||||||
"IPv6": "IPv6",
|
"IPv6": "IPv6",
|
||||||
"Unified Delay": "Общий задержка",
|
"Unified Delay": "Точная задержка",
|
||||||
"Unified Delay Info": "Когда унифицированная задержка включена, будут выполнены два теста задержки, чтобы устранить различия в задержке между разными типами узлов, вызванные подтверждением соединения и т. д",
|
"Unified Delay Info": "Когда унифицированная(точная) задержка включена, будут выполнены два теста задержки, чтобы устранить различия в задержке между разными типами узлов, вызванные подтверждением соединения и т. д",
|
||||||
"Log Level": "Уровень логов",
|
"Log Level": "Уровень логов",
|
||||||
"Log Level Info": "Это действует только на файлы журнала ядра в служебном файле в каталоге журналов.",
|
"Log Level Info": "Это действует только на файлы журнала ядра в служебном файле в каталоге журналов.",
|
||||||
"Port Config": "Настройка порта",
|
"Port Config": "Настройка порта",
|
||||||
@@ -259,32 +274,34 @@
|
|||||||
"Http Port": "Порт Http(s)-прокси",
|
"Http Port": "Порт Http(s)-прокси",
|
||||||
"Redir Port": "Порт прозрачного прокси Redir",
|
"Redir Port": "Порт прозрачного прокси Redir",
|
||||||
"Tproxy Port": "Порт прозрачного прокси Tproxy",
|
"Tproxy Port": "Порт прозрачного прокси Tproxy",
|
||||||
"External": "Внешний",
|
"External": "Внешний контроллер",
|
||||||
"External Controller": "Адрес прослушивания внешнего контроллера",
|
"External Controller": "Адрес прослушивания внешнего контроллера",
|
||||||
"Core Secret": "Секрет",
|
"Core Secret": "Секрет",
|
||||||
"Recommended": "Рекомендуется",
|
"Recommended": "Рекомендуется",
|
||||||
"Open URL": "Открыть URL",
|
"Open URL": "Перейти по адресу",
|
||||||
"Replace host, port, secret with %host, %port, %secret": "Замените хост, порт, секрет на %host, %port, %secret",
|
"Replace host, port, secret with %host, %port, %secret": "Замените хост, порт и секрет на %host, %port, %secret",
|
||||||
"Support %host, %port, %secret": "Поддержка %host, %port, %secret",
|
"Support %host, %port, %secret": "Поддерживаются %host, %port, %secret",
|
||||||
"Clash Core": "Ядра Clash",
|
"Clash Core": "Ядро Clash",
|
||||||
"Upgrade": "Обновлять",
|
"Upgrade": "Обновить",
|
||||||
"Restart": "Перезапуск",
|
"Restart": "Перезапустить",
|
||||||
"Release Version": "Официальная версия",
|
"Release Version": "Официальная версия",
|
||||||
"Alpha Version": "Альфа-версия",
|
"Alpha Version": "Альфа-версия",
|
||||||
"Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания",
|
"Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим системной службы",
|
||||||
"Please enter your root password": "Пожалуйста, введите ваш пароль root",
|
"Please enter your root password": "Пожалуйста, введите ваш пароль root",
|
||||||
"Grant": "Предоставить",
|
"Grant": "Предоставить",
|
||||||
"Open UWP tool": "Открыть UWP инструмент",
|
"Open UWP tool": "Открыть UWP инструмент",
|
||||||
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
|
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
|
||||||
"Update GeoData": "Обновление GeoData",
|
"Update GeoData": "Обновить GeoData",
|
||||||
"Verge Setting": "Настройки Verge",
|
"Verge Basic Setting": "Основные настройки Verge",
|
||||||
|
"Verge Advanced Setting": "Расширенные настройки Verge",
|
||||||
"Language": "Язык",
|
"Language": "Язык",
|
||||||
"Theme Mode": "Режим темы",
|
"Theme Mode": "Цветовая тема",
|
||||||
"theme.light": "Светлая",
|
"theme.light": "Светлая",
|
||||||
"theme.dark": "Тёмная",
|
"theme.dark": "Тёмная",
|
||||||
"theme.system": "Системная",
|
"theme.system": "Системная",
|
||||||
"Tray Click Event": "Событие щелчка в лотке",
|
"Tray Click Event": "Событие при щелчке по иконке в трее",
|
||||||
"Show Main Window": "Показать главное окно",
|
"Show Main Window": "Показать главное окно",
|
||||||
|
"Show Tray Menu": "Показать меню в трее",
|
||||||
"Copy Env Type": "Скопировать тип Env",
|
"Copy Env Type": "Скопировать тип Env",
|
||||||
"Copy Success": "Скопировано",
|
"Copy Success": "Скопировано",
|
||||||
"Start Page": "Главная страница",
|
"Start Page": "Главная страница",
|
||||||
@@ -293,8 +310,8 @@
|
|||||||
"Theme Setting": "Настройки темы",
|
"Theme Setting": "Настройки темы",
|
||||||
"Primary Color": "Основной цвет",
|
"Primary Color": "Основной цвет",
|
||||||
"Secondary Color": "Вторичный цвет",
|
"Secondary Color": "Вторичный цвет",
|
||||||
"Primary Text Color": "Основной текст",
|
"Primary Text": "Первичный текст",
|
||||||
"Secondary Text Color": "Вторичный текст",
|
"Secondary Text": "Вторичный текст",
|
||||||
"Info Color": "Информационный цвет",
|
"Info Color": "Информационный цвет",
|
||||||
"Warning Color": "Цвет предупреждения",
|
"Warning Color": "Цвет предупреждения",
|
||||||
"Error Color": "Цвет ошибки",
|
"Error Color": "Цвет ошибки",
|
||||||
@@ -306,50 +323,53 @@
|
|||||||
"Memory Usage": "Использование памяти",
|
"Memory Usage": "Использование памяти",
|
||||||
"Memory Cleanup": "Нажмите, чтобы очистить память",
|
"Memory Cleanup": "Нажмите, чтобы очистить память",
|
||||||
"Proxy Group Icon": "Иконка Группы прокси",
|
"Proxy Group Icon": "Иконка Группы прокси",
|
||||||
"Nav Icon": "Иконка навигации",
|
"Nav Icon": "Иконки навигации",
|
||||||
"Monochrome": "Монохромный",
|
"Monochrome": "Монохромные",
|
||||||
"Colorful": "Полноцветный",
|
"Colorful": "Цветные",
|
||||||
"Tray Icon": "Иконка лотка",
|
"Tray Icon": "Иконка в трее",
|
||||||
"Common Tray Icon": "Общий значок в лотке",
|
"Common Tray Icon": "Общий значок в трее",
|
||||||
"System Proxy Tray Icon": "Значок системного прокси в лотке",
|
"System Proxy Tray Icon": "Значок системного прокси в трее",
|
||||||
"Tun Tray Icon": "Значок туннеля в лотке",
|
"Tun Tray Icon": "Значок TUN в трее",
|
||||||
"Miscellaneous": "Настройки Прочие",
|
"Miscellaneous": "Расширенные настройки",
|
||||||
"App Log Level": "Уровень журнала приложения",
|
"App Log Level": "Уровень журнала приложения",
|
||||||
"Auto Close Connections": "Автоматическое закрытие соединений",
|
"Auto Close Connections": "Автоматическое закрытие соединений",
|
||||||
"Auto Close Connections Info": "Завершить установленные соединения при изменении выбора группы прокси или режима прокси",
|
"Auto Close Connections Info": "Закрыть установленные соединения при изменении выбора группы прокси или режима прокси",
|
||||||
"Auto Check Update": "Автоматическая проверка обновлений",
|
"Auto Check Update": "Автоматическая проверка обновлений",
|
||||||
"Enable Builtin Enhanced": "Включить встроенные улучшения",
|
"Enable Builtin Enhanced": "Включить встроенные улучшения",
|
||||||
"Enable Builtin Enhanced Info": "Обработка совместимости для файла конфигурации",
|
"Enable Builtin Enhanced Info": "Обработка совместимости для файла конфигурации",
|
||||||
"Proxy Layout Columns": "Количество столбцов в макете прокси",
|
"Proxy Layout Columns": "Количество столбцов в макете прокси",
|
||||||
"Auto Columns": "Авто колонки",
|
"Auto Columns": "Авто колонки",
|
||||||
"Auto Log Clean": "Автоматическая очистка журналов",
|
"Auto Log Clean": "Автоматическая очистка логов",
|
||||||
"Never Clean": "Никогда не очищать",
|
"Never Clean": "Никогда не очищать",
|
||||||
"Retain _n Days": "Сохранять {{n}} дней",
|
"Retain _n Days": "Сохранять {{n}} дней",
|
||||||
"Default Latency Test": "Ссылка на тестирование задержки по умолчанию",
|
"Default Latency Test": "Ссылка на тест задержки",
|
||||||
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
|
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
|
||||||
"Default Latency Timeout": "Таймаут задержки по умолчанию",
|
"Default Latency Timeout": "Таймаут задержки по умолчанию",
|
||||||
"Hotkey Setting": "Настройки клавиатурных сокращений",
|
"Hotkey Setting": "Настройки сочетаний клавиш",
|
||||||
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
|
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
|
||||||
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
|
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
|
||||||
"clash_mode_rule": "Режим правил",
|
"clash_mode_rule": "Режим правил",
|
||||||
"clash_mode_global": "Глобальный режим",
|
"clash_mode_global": "Глобальный режим",
|
||||||
"clash_mode_direct": "Прямой режим",
|
"clash_mode_direct": "Прямой режим",
|
||||||
"toggle_system_proxy": "Включить/Отключить системный прокси",
|
"toggle_system_proxy": "Включить/Отключить системный прокси",
|
||||||
"toggle_tun_mode": "Включить/Отключить режим туннеля",
|
"toggle_tun_mode": "Включить/Отключить режим TUN",
|
||||||
|
"entry_lightweight_mode": "Вход в LightWeight Mode",
|
||||||
"Backup Setting": "Настройки резервного копирования",
|
"Backup Setting": "Настройки резервного копирования",
|
||||||
"Backup Setting Info": "Поддерживает файлы конфигурации резервного копирования WebDAV",
|
"Backup Setting Info": "Поддерживает файлы конфигурации резервного копирования WebDAV",
|
||||||
"Runtime Config": "Используемый конфиг",
|
"Runtime Config": "Используемый конфиг",
|
||||||
"Open Conf Dir": "Открыть папку приложения",
|
"Open Conf Dir": "Открыть папку приложения",
|
||||||
"Open Conf Dir Info": "Если программное обеспечение работает ненормально, сделайте резервную копию и удалите все файлы в этой папке, а затем перезапустите программное обеспечение",
|
"Open Conf Dir Info": "Если программное обеспечение работает неправильно, сделайте резервную копию и удалите все файлы в этой папке, а затем перезапустите ПО",
|
||||||
"Open Core Dir": "Открыть папку ядра",
|
"Open Core Dir": "Открыть папку ядра",
|
||||||
"Open Logs Dir": "Открыть папку логов",
|
"Open Logs Dir": "Открыть папку логов",
|
||||||
"Check for Updates": "Проверить обновления",
|
"Check for Updates": "Проверить обновления",
|
||||||
"Go to Release Page": "Перейти на страницу релизов",
|
"Go to Release Page": "Перейти на страницу релизов",
|
||||||
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную",
|
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените файлы вручную",
|
||||||
"Break Change Update Error": "Это крупное обновление, которое не поддерживает обновление внутри приложения. Пожалуйста, удалите его и загрузите установочный файл вручную.",
|
"Break Change Update Error": "Это крупное обновление, которое не поддерживает обновление внутри приложения. Пожалуйста, удалите его и загрузите установочный файл вручную.",
|
||||||
"Open Dev Tools": "Открыть инструменты разработчика",
|
"Open Dev Tools": "Открыть Dev Tools",
|
||||||
|
"Export Diagnostic Info": "Экспорт диагностической информации",
|
||||||
|
"Export Diagnostic Info For Issue Reporting": "Экспорт диагностической информации для отчета об ошибке",
|
||||||
"Exit": "Выход",
|
"Exit": "Выход",
|
||||||
"Verge Version": "Версия Verge",
|
"Verge Version": "Версия Clash Verge Rev",
|
||||||
"ReadOnly": "Только для чтения",
|
"ReadOnly": "Только для чтения",
|
||||||
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
|
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
|
||||||
"Filter": "Фильтр",
|
"Filter": "Фильтр",
|
||||||
@@ -359,23 +379,24 @@
|
|||||||
"Use Regular Expression": "Использовать регулярные выражения",
|
"Use Regular Expression": "Использовать регулярные выражения",
|
||||||
"Profile Imported Successfully": "Профиль успешно импортирован",
|
"Profile Imported Successfully": "Профиль успешно импортирован",
|
||||||
"Profile Switched": "Профиль изменен",
|
"Profile Switched": "Профиль изменен",
|
||||||
"Profile Reactivated": "Профиль повторно активирован",
|
"Profile Reactivated": "Профиль перезапущен",
|
||||||
"Only YAML Files Supported": "Поддерживаются только файлы YAML",
|
"Only YAML Files Supported": "Поддерживаются только файлы YAML",
|
||||||
"Settings Applied": "Применены настройки",
|
"Settings Applied": "Настройки применены",
|
||||||
|
"Installing Service...": "Установка службы...",
|
||||||
"Service Installed Successfully": "Служба успешно установлена",
|
"Service Installed Successfully": "Служба успешно установлена",
|
||||||
"Service Uninstalled Successfully": "Служба успешно удалена",
|
"Service Uninstalled Successfully": "Служба успешно удалена",
|
||||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "Продолжительность работы прокси-демона не может быть меньше 1 секунды",
|
"Proxy Daemon Duration Cannot be Less than 1 Second": "Продолжительность работы прокси-демона не может быть меньше 1 секунды",
|
||||||
"Invalid Bypass Format": "Неверный формат обхода",
|
"Invalid Bypass Format": "Неверный формат обхода",
|
||||||
"Clash Port Modified": "Clash порт изменен",
|
"Clash Port Modified": "Порт Clash изменен",
|
||||||
"Port Conflict": "Конфликт портов",
|
"Port Conflict": "Конфликт портов",
|
||||||
"Restart Application to Apply Modifications": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
"Restart Application to Apply Modifications": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
||||||
"External Controller Address Modified": "Изменен адрес внешнего контроллера",
|
"External Controller Address Modified": "Настройки внешнего контроллера изменены",
|
||||||
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
|
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
|
||||||
"Core Version Updated": "Обновлена версия ядра",
|
"Core Version Updated": "Ядро обновлено до последней версии",
|
||||||
"Clash Core Restarted": "Clash ядра перезапущено",
|
"Clash Core Restarted": "Ядро перезапущено",
|
||||||
"GeoData Updated": "GeoData Обновлена",
|
"GeoData Updated": "Файлы GeoData обновлены",
|
||||||
"Currently on the Latest Version": "В настоящее время используется последняя версия",
|
"Currently on the Latest Version": "Обновление не требуется",
|
||||||
"Import subscription successful": "Импорт подписки успешно",
|
"Import Subscription Successful": "Подписка успешно импортирована",
|
||||||
"WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://",
|
"WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://",
|
||||||
"Username": "Имя пользователя",
|
"Username": "Имя пользователя",
|
||||||
"Password": "Пароль",
|
"Password": "Пароль",
|
||||||
@@ -410,12 +431,12 @@
|
|||||||
"PAC File": "PAC файл",
|
"PAC File": "PAC файл",
|
||||||
"Web UI": "Веб-интерфейс",
|
"Web UI": "Веб-интерфейс",
|
||||||
"Hotkeys": "Горячие клавиши",
|
"Hotkeys": "Горячие клавиши",
|
||||||
"Verge Mixed Port": "Смешанный порт Verge",
|
"Verge Mixed Port": "Mixed порт",
|
||||||
"Verge Socks Port": "Порт Verge Socks",
|
"Verge Socks Port": "Порт Socks",
|
||||||
"Verge Redir Port": "Порт перенаправления Verge",
|
"Verge Redir Port": "Порт Redir",
|
||||||
"Verge Tproxy Port": "Порт Verge Tproxy",
|
"Verge Tproxy Port": "Порт Tproxy",
|
||||||
"Verge Port": "Порт Verge",
|
"Verge Port": "Порт Verge",
|
||||||
"Verge HTTP Enabled": "HTTP Verge включен",
|
"Verge HTTP Enabled": "HTTP включен",
|
||||||
"WebDAV URL": "URL WebDAV",
|
"WebDAV URL": "URL WebDAV",
|
||||||
"WebDAV Username": "Имя пользователя WebDAV",
|
"WebDAV Username": "Имя пользователя WebDAV",
|
||||||
"WebDAV Password": "Пароль WebDAV",
|
"WebDAV Password": "Пароль WebDAV",
|
||||||
@@ -432,9 +453,16 @@
|
|||||||
"Rule Mode": "Режим правил",
|
"Rule Mode": "Режим правил",
|
||||||
"Global Mode": "Глобальный режим",
|
"Global Mode": "Глобальный режим",
|
||||||
"Direct Mode": "Прямой режим",
|
"Direct Mode": "Прямой режим",
|
||||||
"Enable Tray Speed": "Включить скорость в лотке",
|
"Enable Tray Speed": "Показывать скорость в трее",
|
||||||
"LightWeight Mode": "Облегченный режим",
|
"Enable Tray Icon": "Показывать значок в трее",
|
||||||
"LightWeight Mode Info": "Закройте графический интерфейс и оставьте работать только ядро",
|
"LightWeight Mode": "LightWeight Mode",
|
||||||
|
"LightWeight Mode Info": "Режим, в котором работает только ядро Clash, а графический интрефейс закрыт",
|
||||||
|
"LightWeight Mode Settings": "Настройки LightWeight Mode",
|
||||||
|
"Enter LightWeight Mode Now": "Войти в LightWeight Mode",
|
||||||
|
"Auto Enter LightWeight Mode": "Автоматический вход в LightWeight Mode",
|
||||||
|
"Auto Enter LightWeight Mode Info": "Автоматически включать LightWeight Mode, если окно закрыто определенное время",
|
||||||
|
"Auto Enter LightWeight Mode Delay": "Задержка включения LightWeight Mode",
|
||||||
|
"When closing the window, LightWeight Mode will be automatically activated after _n minutes": "При закрытии окна LightWeight Mode будет автоматически активирован через {{n}} минут",
|
||||||
"Config Validation Failed": "Ошибка проверки конфигурации подписки, проверьте файл конфигурации, изменения отменены, ошибка:",
|
"Config Validation Failed": "Ошибка проверки конфигурации подписки, проверьте файл конфигурации, изменения отменены, ошибка:",
|
||||||
"Boot Config Validation Failed": "Ошибка проверки конфигурации при запуске, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
"Boot Config Validation Failed": "Ошибка проверки конфигурации при запуске, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
||||||
"Core Change Config Validation Failed": "Ошибка проверки конфигурации при смене ядра, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
"Core Change Config Validation Failed": "Ошибка проверки конфигурации при смене ядра, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
||||||
@@ -443,12 +471,112 @@
|
|||||||
"Script Missing Main": "Ошибка скрипта, изменения отменены",
|
"Script Missing Main": "Ошибка скрипта, изменения отменены",
|
||||||
"File Not Found": "Файл не найден, изменения отменены",
|
"File Not Found": "Файл не найден, изменения отменены",
|
||||||
"Script File Error": "Ошибка файла скрипта, изменения отменены",
|
"Script File Error": "Ошибка файла скрипта, изменения отменены",
|
||||||
"Core Changed Successfully": "Ядро успешно сменено",
|
"Core Changed Successfully": "Ядро успешно изменено",
|
||||||
"Failed to Change Core": "Не удалось сменить ядро",
|
"Failed to Change Core": "Не удалось сменить ядро",
|
||||||
"Verge Basic Setting": "Основные настройки Verge",
|
"YAML Syntax Error": "Ошибка синтаксиса YAML, откат изменений",
|
||||||
"Verge Advanced Setting": "Расширенные настройки Verge",
|
"YAML Read Error": "Ошибка чтения YAML, откат изменений",
|
||||||
"TUN requires Service Mode": "Режим TUN требует обслуживания",
|
"YAML Mapping Error": "Ошибка YAML Mapping, откат изменений",
|
||||||
"Install Service": "Установить службу",
|
"YAML Key Error": "Ошибка ключа YAML, откат изменений",
|
||||||
"Installing Service...": "Установка службы...",
|
"YAML Error": "Ошибка YAML, откат изменений",
|
||||||
"Service Administrator Prompt": "Clash Verge требует прав администратора для переустановки системной службы"
|
"Merge File Syntax Error": "Ошибка синтаксиса Merge File, откат изменений",
|
||||||
|
"Merge File Mapping Error": "Ошибка сопоставления в Merge File, откат изменений",
|
||||||
|
"Merge File Key Error": "Ошибка ключа в Merge File, откат изменений",
|
||||||
|
"Merge File Error": "Ошибка Merge File, откат изменений",
|
||||||
|
"Validate YAML File": "Проверить YAML файл",
|
||||||
|
"Validate Merge File": "Проверить Merge File",
|
||||||
|
"Validation Success": "Файл успешно проверен",
|
||||||
|
"Validation Failed": "Проверка не удалась",
|
||||||
|
"Service Administrator Prompt": "Clash Verge требует прав администратора для переустановки системной службы",
|
||||||
|
"DNS Settings": "Настройки DNS",
|
||||||
|
"DNS Overwrite": "Переопределение настроек DNS",
|
||||||
|
"DNS Settings Warning": "Если вы не знакомы с этими настройками, пожалуйста, не изменяйте и не отключайте их",
|
||||||
|
"Enable DNS": "Включить DNS",
|
||||||
|
"DNS Listen": "Прослушивание DNS",
|
||||||
|
"Enhanced Mode": "Enhanced Mode",
|
||||||
|
"Fake IP Range": "Диапазон FakeIP",
|
||||||
|
"Fake IP Filter Mode": "FakeIP Filter Mode",
|
||||||
|
"Prefer H3": "Предпочитать H3",
|
||||||
|
"DNS DOH使用HTTP/3": "DNS DOH использует http/3",
|
||||||
|
"Respect Rules": "Приоритизировать правила",
|
||||||
|
"DNS连接遵守路由规则": "Соединения DNS следуют правилам маршрутизации",
|
||||||
|
"Use Hosts": "Использовать файл Hosts",
|
||||||
|
"Enable to resolve hosts through hosts file": "Включить разрешение хостов через файл Hosts",
|
||||||
|
"Use System Hosts": "Использовать системный файл Hosts",
|
||||||
|
"Enable to resolve hosts through system hosts file": "Включить разрешение хостов через системный файл Hosts",
|
||||||
|
"Direct Nameserver Follow Policy": "Прямой сервер имен следует политике",
|
||||||
|
"是否遵循nameserver-policy": "Следовать ли политике DNS-серверов",
|
||||||
|
"Default Nameserver": "DNS-сервер по умолчанию",
|
||||||
|
"Default DNS servers used to resolve DNS servers": "DNS-серверы по умолчанию, используемые для разрешения адресов серверов DNS",
|
||||||
|
"Nameserver": "DNS-сервер",
|
||||||
|
"List of DNS servers": "Список DNS-серверов, разделенных запятой",
|
||||||
|
"Fallback": "Fallback",
|
||||||
|
"List of fallback DNS servers": "Список резервных DNS-серверов, разделенных запятой",
|
||||||
|
"Proxy Server Nameserver": "Proxy Server Nameserver",
|
||||||
|
"Proxy Node Nameserver": "DNS-серверы для разрешения домена прокси-узлов",
|
||||||
|
"Direct Nameserver": "DNS-сервер для прямых соединений",
|
||||||
|
"Direct outbound Nameserver": "Список DNS-серверов для прямых соединений, разделенных запятой",
|
||||||
|
"Fake IP Filter": "Фильтр FakeIP",
|
||||||
|
"Domains that skip fake IP resolution": "Домены, которые пропускают разрешение FakeIP, разделенные запятой",
|
||||||
|
"Nameserver Policy": "Политика серверов имен",
|
||||||
|
"Domain-specific DNS server": "DNS-сервер, специфичный для домена, несколько серверов разделяются знаком ';'",
|
||||||
|
"Fallback Filter Settings": "Настройки фильтра Fallback",
|
||||||
|
"GeoIP Filtering": "Фильтрация GeoIP",
|
||||||
|
"Enable GeoIP filtering for fallback": "Включить фильтрацию GeoIP",
|
||||||
|
"GeoIP Code": "Код GeoIP",
|
||||||
|
"Fallback IP CIDR": "Fallback IP CIDR",
|
||||||
|
"IP CIDRs not using fallback servers": "Диапазоны IP-адресов, не использующие резервные серверы, разделенные запятой",
|
||||||
|
"Fallback Domain": "Fallback домены",
|
||||||
|
"Domains using fallback servers": "Домены, использующие резервные серверы, разделенные запятой",
|
||||||
|
"Enable Alpha Channel": "Включить альфа-канал",
|
||||||
|
"Alpha versions may contain experimental features and bugs": "Альфа-версии могут содержать экспериментальные функции и ошибки",
|
||||||
|
"Home Settings": "Настройки главной страницы",
|
||||||
|
"Profile Card": "Карточка профиля",
|
||||||
|
"Current Proxy Card": "Карточка текущего прокси",
|
||||||
|
"Network Settings Card": "Карточка настроек сети",
|
||||||
|
"Proxy Mode Card": "Карточка режима работы",
|
||||||
|
"Clash Mode Card": "Карточка режима Clash",
|
||||||
|
"Traffic Stats Card": "Карточка статистики по трафику",
|
||||||
|
"Clash Info Cards": "Информация о Clash",
|
||||||
|
"System Info Cards": "Информация о системе",
|
||||||
|
"Website Tests Card": "Карточка тестов доступности веб-сайтов",
|
||||||
|
"Traffic Stats": "Статистика по трафику",
|
||||||
|
"Website Tests": "Проверка доступности веб-сайтов",
|
||||||
|
"Clash Info": "Информация о Clash",
|
||||||
|
"Core Version": "Версия ядра",
|
||||||
|
"System Proxy Address": "Адрес системного прокси",
|
||||||
|
"Uptime": "Время работы",
|
||||||
|
"Rules Count": "Количество правил",
|
||||||
|
"System Info": "Информация о системе",
|
||||||
|
"OS Info": "Версия ОС",
|
||||||
|
"Running Mode": "Режим работы",
|
||||||
|
"Sidecar Mode": "Пользовательский режим",
|
||||||
|
"Last Check Update": "Последняя проверка обновлений",
|
||||||
|
"Click to import subscription": "Нажмите, чтобы импортировать подписку",
|
||||||
|
"Update subscription successfully": "Подписка успешно обновлена",
|
||||||
|
"Current Node": "Текущий сервер",
|
||||||
|
"No active proxy node": "Нет активного прокси-узла",
|
||||||
|
"Network Settings": "Настройки сети",
|
||||||
|
"Proxy Mode": "Режим работы",
|
||||||
|
"Group": "Группа",
|
||||||
|
"Proxy": "Прокси",
|
||||||
|
"IP Information Card": "Информация об IP",
|
||||||
|
"IP Information": "Информация об IP",
|
||||||
|
"Failed to get IP info": "Не удалось получить информацию об IP",
|
||||||
|
"ISP": "ISP",
|
||||||
|
"ASN": "ASN",
|
||||||
|
"ORG": "ORG",
|
||||||
|
"Location": "Location",
|
||||||
|
"Timezone": "Timezone",
|
||||||
|
"Auto refresh": "Автоматическое обновление через",
|
||||||
|
"Unlock Test": "Тест доступности веб-сайтов",
|
||||||
|
"Pending": "В ожидании",
|
||||||
|
"Yes": "Да",
|
||||||
|
"No": "Нет",
|
||||||
|
"Failed": "Ошибка",
|
||||||
|
"Completed": "Завершено",
|
||||||
|
"Disallowed ISP": "ISP заблокирован",
|
||||||
|
"Originals Only": "Только Originals",
|
||||||
|
"No (IP Banned By Disney+)": "Нет (IP забанен Disney+)",
|
||||||
|
"Unsupported Country": "Страна не поддерживается",
|
||||||
|
"Failed (Network Connection)": "Ошибка подключения"
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -205,8 +205,10 @@
|
|||||||
"Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
|
"Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
|
||||||
"System Proxy Enabled": "系统代理已启用,您的应用将通过代理访问网络",
|
"System Proxy Enabled": "系统代理已启用,您的应用将通过代理访问网络",
|
||||||
"System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项",
|
"System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项",
|
||||||
|
"TUN Mode Enabled": "TUN 模式已启用,应用将通过虚拟网卡访问网络",
|
||||||
|
"TUN Mode Disabled": "TUN 模式已关闭,适用于特殊应用",
|
||||||
"TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务",
|
"TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务",
|
||||||
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊应用",
|
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊不遵循系统代理设置的应用",
|
||||||
"rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
"rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
||||||
"global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
"global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
||||||
"direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
"direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
||||||
@@ -351,6 +353,7 @@
|
|||||||
"clash_mode_direct": "直连模式",
|
"clash_mode_direct": "直连模式",
|
||||||
"toggle_system_proxy": "打开/关闭系统代理",
|
"toggle_system_proxy": "打开/关闭系统代理",
|
||||||
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
||||||
|
"entry_lightweight_mode": "进入轻量模式",
|
||||||
"Backup Setting": "备份设置",
|
"Backup Setting": "备份设置",
|
||||||
"Backup Setting Info": "支持 WebDAV 备份配置文件",
|
"Backup Setting Info": "支持 WebDAV 备份配置文件",
|
||||||
"Runtime Config": "当前配置",
|
"Runtime Config": "当前配置",
|
||||||
@@ -451,6 +454,7 @@
|
|||||||
"Global Mode": "全局模式",
|
"Global Mode": "全局模式",
|
||||||
"Direct Mode": "直连模式",
|
"Direct Mode": "直连模式",
|
||||||
"Enable Tray Speed": "启用托盘速率",
|
"Enable Tray Speed": "启用托盘速率",
|
||||||
|
"Enable Tray Icon": "启用托盘图标",
|
||||||
"LightWeight Mode": "轻量模式",
|
"LightWeight Mode": "轻量模式",
|
||||||
"LightWeight Mode Info": "关闭GUI界面,仅保留内核运行",
|
"LightWeight Mode Info": "关闭GUI界面,仅保留内核运行",
|
||||||
"LightWeight Mode Settings": "轻量模式设置",
|
"LightWeight Mode Settings": "轻量模式设置",
|
||||||
@@ -570,7 +574,6 @@
|
|||||||
"No": "不支持",
|
"No": "不支持",
|
||||||
"Failed": "测试失败",
|
"Failed": "测试失败",
|
||||||
"Completed": "检测完成",
|
"Completed": "检测完成",
|
||||||
"Bahamut Anime": "动画疯",
|
|
||||||
"Disallowed ISP": "不允许的 ISP",
|
"Disallowed ISP": "不允许的 ISP",
|
||||||
"Originals Only": "仅限原创",
|
"Originals Only": "仅限原创",
|
||||||
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
|
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
|
||||||
|
|||||||
@@ -249,8 +249,8 @@ const Layout = () => {
|
|||||||
? {
|
? {
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
border: "1px solid var(--divider-color)",
|
border: "1px solid var(--divider-color)",
|
||||||
width: "calc(100vw - 0px)",
|
width: "calc(100vw - 4px)",
|
||||||
height: "calc(100vh - 0px)",
|
height: "calc(100vh - 4px)",
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
]}
|
]}
|
||||||
|
|||||||
+10
-2
@@ -37,7 +37,11 @@ import { BasePage } from "@/components/base";
|
|||||||
import { ClashInfoCard } from "@/components/home/clash-info-card";
|
import { ClashInfoCard } from "@/components/home/clash-info-card";
|
||||||
import { SystemInfoCard } from "@/components/home/system-info-card";
|
import { SystemInfoCard } from "@/components/home/system-info-card";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { entry_lightweight_mode, openWebUrl, patchVergeConfig } from "@/services/cmds";
|
import {
|
||||||
|
entry_lightweight_mode,
|
||||||
|
openWebUrl,
|
||||||
|
patchVergeConfig,
|
||||||
|
} from "@/services/cmds";
|
||||||
import { TestCard } from "@/components/home/test-card";
|
import { TestCard } from "@/components/home/test-card";
|
||||||
import { IpInfoCard } from "@/components/home/ip-info-card";
|
import { IpInfoCard } from "@/components/home/ip-info-card";
|
||||||
|
|
||||||
@@ -260,7 +264,11 @@ const HomePage = () => {
|
|||||||
header={
|
header={
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Tooltip title={t("LightWeight Mode")} arrow>
|
<Tooltip title={t("LightWeight Mode")} arrow>
|
||||||
<IconButton onClick={() => entry_lightweight_mode()} size="small" color="inherit">
|
<IconButton
|
||||||
|
onClick={async () => await entry_lightweight_mode()}
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
<HistoryEduOutlined />
|
<HistoryEduOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ const ProxyPage = () => {
|
|||||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||||
"getClashConfig",
|
"getClashConfig",
|
||||||
getClashConfig,
|
getClashConfig,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: true,
|
||||||
|
dedupingInterval: 1000,
|
||||||
|
errorRetryInterval: 5000
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
|||||||
Vendored
+1
@@ -738,6 +738,7 @@ interface IVergeConfig {
|
|||||||
sysproxy_tray_icon?: boolean;
|
sysproxy_tray_icon?: boolean;
|
||||||
tun_tray_icon?: boolean;
|
tun_tray_icon?: boolean;
|
||||||
enable_tray_speed?: boolean;
|
enable_tray_speed?: boolean;
|
||||||
|
enable_tray_icon?: boolean;
|
||||||
enable_tun_mode?: boolean;
|
enable_tun_mode?: boolean;
|
||||||
enable_auto_light_weight_mode?: boolean;
|
enable_auto_light_weight_mode?: boolean;
|
||||||
auto_light_weight_minutes?: number;
|
auto_light_weight_minutes?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user