Compare commits
48 Commits
21
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
21
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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)
|
||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
||||
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
|
||||
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
|
||||
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
|
||||
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
|
||||
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
@@ -55,17 +57,6 @@ body:
|
||||
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: os-labels
|
||||
attributes:
|
||||
label: 系统标签 / OS Labels
|
||||
description: 请选择受影响的操作系统(至少选择一个) / Please select the affected operating system(s) (select at least one)
|
||||
options:
|
||||
- label: windows
|
||||
- label: macos
|
||||
- label: linux
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志 / Log
|
||||
|
||||
248
.github/workflows/alpha.yml
vendored
248
.github/workflows/alpha.yml
vendored
@@ -3,8 +3,8 @@ name: Alpha Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 00:00 (UTC 16:00 previous day) and UTC+8 12:00 (UTC 04:00)
|
||||
- cron: "0 16,4 * * *"
|
||||
# UTC+8 0,6,12,18
|
||||
- cron: "0 16,22,4,10 * * *"
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if commit changed
|
||||
- name: Check if version changed
|
||||
id: check
|
||||
run: |
|
||||
# For manual workflow_dispatch, always run
|
||||
@@ -34,21 +34,165 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if current commit is different from the previous one
|
||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||
PREVIOUS_COMMIT=$(git rev-parse HEAD~1)
|
||||
# Store current version from package.json
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
if [ "$CURRENT_COMMIT" != "$PREVIOUS_COMMIT" ]; then
|
||||
echo "New commit detected: $CURRENT_COMMIT"
|
||||
# Get the previous commit's package.json version
|
||||
git checkout HEAD~1 package.json
|
||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Previous version: $PREVIOUS_VERSION"
|
||||
|
||||
# Reset back to current commit
|
||||
git checkout HEAD package.json
|
||||
|
||||
# Check if version changed
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No new commits since last run"
|
||||
echo "Version unchanged: $CURRENT_VERSION"
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
alpha:
|
||||
delete_old_assets:
|
||||
needs: check_commit
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Release Assets
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const releaseTag = 'alpha';
|
||||
|
||||
try {
|
||||
// Get the release by tag name
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag: releaseTag
|
||||
});
|
||||
|
||||
console.log(`Found release with ID: ${release.id}`);
|
||||
|
||||
// Delete each asset
|
||||
if (release.assets && release.assets.length > 0) {
|
||||
console.log(`Deleting ${release.assets.length} assets`);
|
||||
|
||||
for (const asset of release.assets) {
|
||||
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
asset_id: asset.id
|
||||
});
|
||||
}
|
||||
|
||||
console.log('All assets deleted successfully');
|
||||
} else {
|
||||
console.log('No assets found to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log('Release not found, nothing to delete');
|
||||
} else {
|
||||
console.error('Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: delete_old_assets
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch Alpha update logs
|
||||
id: fetch_alpha_logs
|
||||
run: |
|
||||
# Check if UPDATELOG.md exists
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
# Extract the section starting with ## and containing -alpha until the next ## or end of file
|
||||
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
|
||||
if [ -n "$ALPHA_LOGS" ]; then
|
||||
echo "Found alpha update logs"
|
||||
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$ALPHA_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No alpha sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息
|
||||
if [ -z "$ALPHA_LOGS" ]; then
|
||||
echo "No alpha logs found, using default message"
|
||||
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found alpha logs"
|
||||
fi
|
||||
|
||||
# 生成 release.txt 文件
|
||||
cat > release.txt << EOF
|
||||
$ALPHA_LOGS
|
||||
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- 64位: x64-setup.exe
|
||||
- arm64架构: arm64-setup.exe
|
||||
#### 便携版问题很多不再提供
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- 64位: x64_fixed_webview2-setup.exe
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
|
||||
### FAQ
|
||||
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
alpha:
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -86,7 +230,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -103,8 +247,8 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Alpha Version update
|
||||
run: pnpm run fix-alpha-version
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@@ -129,8 +273,7 @@ jobs:
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
alpha-for-linux-arm:
|
||||
needs: check_commit
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -173,6 +316,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
sudo ls -lR /etc/apt/
|
||||
@@ -196,6 +342,7 @@ jobs:
|
||||
sudo apt update
|
||||
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
@@ -244,7 +391,6 @@ jobs:
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
@@ -252,8 +398,7 @@ jobs:
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
alpha-for-fixed-webview2:
|
||||
needs: check_commit
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -294,6 +439,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||
@@ -338,7 +486,6 @@ jobs:
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
@@ -347,66 +494,3 @@ jobs:
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check_commit, alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# - name: Update Tag
|
||||
# uses: richardsimko/update-tag@v1
|
||||
# with:
|
||||
# tag_name: alpha
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: |
|
||||
cat > release.txt << 'EOF'
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- 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
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -153,6 +153,7 @@ jobs:
|
||||
sudo apt update
|
||||
|
||||
sudo apt install -y \
|
||||
libxslt1.1:${{ matrix.arch }} \
|
||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||
libssl-dev:${{ matrix.arch }} \
|
||||
|
||||
64
UPDATELOG.md
64
UPDATELOG.md
@@ -1,9 +1,44 @@
|
||||
## v2.2.0
|
||||
## v2.2.1
|
||||
|
||||
**发行代号:拓**
|
||||
|
||||
感谢 Tunglies 对 Verge 后端重构,性能优化做出的重大贡献!
|
||||
|
||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||
|
||||
2.2.1 相对于 2.2.0(已下架不在提供)
|
||||
修复了:
|
||||
1. **首页**
|
||||
- 修复 Direct 模式首页无法渲染
|
||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||
- 修复 系统代理标识判断不准的问题
|
||||
- 修复 系统代理地址错误的问题
|
||||
- 代理模式“多余的切换动画”
|
||||
2. **系统**
|
||||
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
|
||||
- 修复 代理端口设置同步问题。
|
||||
- 修复 Linux 无法与 Mihomo 核心 和 ClashVergeRev 服务通信
|
||||
3. **界面**
|
||||
- 修复 连接详情卡没有跟随主题色
|
||||
4. **轻量模式**
|
||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||
|
||||
新增了:
|
||||
1. **首页**
|
||||
- 首页文本过长自动截断
|
||||
2. **轻量模式**
|
||||
- 新增托盘进入轻量模式支持
|
||||
- 新增进入轻量模式快捷键支持
|
||||
3. **系统**
|
||||
- 在 ClashVergeRev 对 Mihomo 进行操作时,总是尝试确保两者运行
|
||||
- 服务器模式下启动mihomo内核的时候查找并停止其他已经存在的内核进程,防止内核假死等问题带来的通信失败
|
||||
4. **托盘**
|
||||
- 新增 MacOS 启用托盘速率显示时,可选隐藏托盘图标显示
|
||||
|
||||
---
|
||||
|
||||
## v2.2.0(已下架不在提供)
|
||||
|
||||
#### 新增功能
|
||||
1. **首页**
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
@@ -13,8 +48,8 @@
|
||||
- 限制首页配置文件卡片URL长度。
|
||||
|
||||
2. **DNS 设置与覆写**
|
||||
- 默认启用 DNS 设置。
|
||||
- 新增 DNS 覆写功能。
|
||||
- 默认启用 DNS 覆写。
|
||||
|
||||
3. **解锁测试**
|
||||
- 新增解锁测试页面。
|
||||
@@ -24,11 +59,11 @@
|
||||
- 添加自动轻量模式定时器。
|
||||
|
||||
5. **系统支持**
|
||||
- Mihomo(meta)内核升级 1.19.3
|
||||
- macOS 支持 CMD+W 关闭窗口。
|
||||
- 新增 macOS 应用菜单。
|
||||
- 支持 alpha 更新。
|
||||
- 添加管理员权限提示。
|
||||
- 新增 sidecar 模式。
|
||||
- 添加 macOS 安装服务时候的管理员权限提示。
|
||||
- 新增 sidecar(用户空间启动内核) 模式。
|
||||
|
||||
6. **其他**
|
||||
- 增强延迟测试日志和错误处理。
|
||||
@@ -39,26 +74,29 @@
|
||||
1. **系统**
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
- 修复 macOS 静默启动崩溃。
|
||||
- 修复 macOS tray图标错位到左上角的问题。
|
||||
- 修复 Windows/Linux 运行时崩溃。
|
||||
- 修复 Netflix 检测错误。
|
||||
- 修复服务模式检测失败。
|
||||
- 修复 Win10 阴影和边框问题。
|
||||
|
||||
2. **性能**
|
||||
- 优化小数值速度更新。
|
||||
- 增加请求超时至 60 秒。
|
||||
- 修复代理节点选择同步。
|
||||
|
||||
3. **构建**
|
||||
2. **构建**
|
||||
- 修复构建失败问题。
|
||||
|
||||
#### 优化
|
||||
1. **性能**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
- 优化流量图表资源使用。
|
||||
- 提升代理组列表滚动性能。
|
||||
- 加快应用退出速度。
|
||||
- 加快进入轻量模式速度。
|
||||
- 优化小数值速度更新。
|
||||
- 增加请求超时至 60 秒。
|
||||
- 修复代理节点选择同步。
|
||||
- 优化修改verge配置性能。
|
||||
|
||||
2. **重构**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化定时器管理。
|
||||
- 重构 MihomoManager 处理流量。
|
||||
- 优化 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 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||
@@ -16,7 +16,8 @@
|
||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||
"prepare": "husky",
|
||||
"clean": "cd ./src-tauri && cargo clean && cd -"
|
||||
},
|
||||
|
||||
96
scripts/release-alpha_version.mjs
Normal file
96
scripts/release-alpha_version.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 更新 package.json 文件中的版本号
|
||||
*/
|
||||
async function updatePackageVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
let result = packageJson.version;
|
||||
if (!result.includes("alpha")) {
|
||||
result = `${result}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current package.json version is: ", result);
|
||||
packageJson.version = result;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: package.json version updated to: ${result}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Cargo.toml 文件中的版本号
|
||||
*/
|
||||
async function updateCargoVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.startsWith("version =")) {
|
||||
const versionMatch = line.match(/version\s*=\s*"([^"]+)"/);
|
||||
if (versionMatch && !versionMatch[1].includes("alpha")) {
|
||||
const newVersion = `${versionMatch[1]}-alpha`;
|
||||
return line.replace(versionMatch[1], newVersion);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 tauri.conf.json 文件中的版本号
|
||||
*/
|
||||
async function updateTauriConfigVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
|
||||
let version = tauriConfig.version;
|
||||
if (!version.includes("alpha")) {
|
||||
version = `${version}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current tauri.conf.json version is: ", version);
|
||||
tauriConfig.version = version;
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: tauri.conf.json version updated to: ${version}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数,依次更新所有文件的版本号
|
||||
*/
|
||||
async function main() {
|
||||
await updatePackageVersion();
|
||||
await updateCargoVersion();
|
||||
await updateTauriConfigVersion();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -43,3 +43,42 @@ export async function resolveUpdateLog(tag) {
|
||||
|
||||
return map[tag].join("\n").trim();
|
||||
}
|
||||
|
||||
export async function resolveUpdateLogDefault() {
|
||||
const cwd = process.cwd();
|
||||
const file = path.join(cwd, UPDATE_LOG);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error("could not found UPDATELOG.md");
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(file, "utf-8");
|
||||
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
let content = [];
|
||||
let firstTag = "";
|
||||
|
||||
for (const line of data.split("\n")) {
|
||||
if (reTitle.test(line) && !isCapturing) {
|
||||
isCapturing = true;
|
||||
firstTag = line.slice(3).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCapturing) {
|
||||
if (reEnd.test(line)) {
|
||||
break;
|
||||
}
|
||||
content.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstTag) {
|
||||
throw new Error("could not found any version tag in UPDATELOG.md");
|
||||
}
|
||||
|
||||
return content.join("\n").trim();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||
|
||||
// Add stable update JSON filenames
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
@@ -85,8 +85,8 @@ async function processRelease(github, options, tag, isAlpha) {
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name).catch(
|
||||
() => "No changelog available",
|
||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||
),
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
|
||||
13
src-tauri/Cargo.lock
generated
13
src-tauri/Cargo.lock
generated
@@ -1132,7 +1132,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.1.2"
|
||||
version = "2.2.1"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"aes-gcm",
|
||||
@@ -1146,6 +1146,7 @@ dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"env_logger",
|
||||
"fs2",
|
||||
"futures",
|
||||
"getrandom 0.3.2",
|
||||
"image",
|
||||
@@ -2339,6 +2340,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.1.2"
|
||||
version = "2.2.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -34,6 +34,7 @@ port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
fs2 = "0.4.3"
|
||||
window-shadows = { version = "0.2.2" }
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
8
src-tauri/src/cmd/core.rs
Normal file
8
src-tauri/src/cmd/core.rs
Normal file
@@ -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 {
|
||||
lightweight::entry_lightweight_mode();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub type CmdResult<T = ()> = Result<T, String>;
|
||||
// Command modules
|
||||
pub mod app;
|
||||
pub mod clash;
|
||||
pub mod core;
|
||||
pub mod media_unlock_checker;
|
||||
pub mod network;
|
||||
pub mod profile;
|
||||
@@ -22,6 +23,7 @@ pub mod lighteweight;
|
||||
// Re-export all command functions for backwards compatibility
|
||||
pub use app::*;
|
||||
pub use clash::*;
|
||||
pub use core::*;
|
||||
pub use media_unlock_checker::*;
|
||||
pub use network::*;
|
||||
pub use profile::*;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::CmdResult;
|
||||
use crate::module::mihomo::MihomoManager;
|
||||
use crate::{core::CoreManager, module::mihomo::MihomoManager};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
let mannager = MihomoManager::global();
|
||||
let proxies = mannager
|
||||
.refresh_proxies()
|
||||
@@ -14,6 +15,7 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
let mannager = MihomoManager::global();
|
||||
let providers = mannager
|
||||
.refresh_providers_proxies()
|
||||
|
||||
@@ -189,11 +189,16 @@ pub struct IVerge {
|
||||
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式的延迟(分钟)
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
|
||||
/// 服务状态跟踪
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -295,11 +300,13 @@ impl IVerge {
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(true),
|
||||
enable_tray_icon: Some(true),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
enable_dns_settings: Some(true),
|
||||
home_cards: None,
|
||||
service_state: None,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -381,10 +388,12 @@ impl IVerge {
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
patch!(enable_tray_icon);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(service_state);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
@@ -473,10 +482,12 @@ pub struct IVergeResponse {
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
@@ -539,10 +550,12 @@ impl From<IVerge> for IVergeResponse {
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
enable_tray_icon: verge.enable_tray_icon,
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
service_state: verge.service_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,14 @@ use crate::{
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use fs2::FileExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::{sync::Mutex, time::sleep};
|
||||
|
||||
use super::service::is_service_running;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
running: Arc<Mutex<bool>>,
|
||||
@@ -50,10 +53,13 @@ impl CoreManager {
|
||||
let mut running = self.running.lock().await;
|
||||
|
||||
if !*running {
|
||||
println!("[停止内核] 内核未运行");
|
||||
log::debug!("core is not running");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("[停止内核] 开始停止内核");
|
||||
|
||||
// 关闭tun模式
|
||||
// Create a JSON object to disable TUN mode
|
||||
let disable = serde_json::json!({
|
||||
@@ -61,17 +67,22 @@ impl CoreManager {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
println!("[停止内核] 禁用TUN模式");
|
||||
log::debug!(target: "app", "disable tun mode");
|
||||
log_err!(MihomoManager::global().patch_configs(disable).await);
|
||||
|
||||
// 服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
println!("[停止内核] 尝试通过服务停止内核");
|
||||
log::info!(target: "app", "stop the core by service");
|
||||
match service::stop_core_by_service().await {
|
||||
Ok(_) => {
|
||||
println!("[停止内核] 服务模式下内核停止成功");
|
||||
log::info!(target: "app", "core stopped successfully by service");
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[停止内核] 服务模式下停止内核失败: {}", err);
|
||||
println!("[停止内核] 尝试停止可能的sidecar进程");
|
||||
log::warn!(target: "app", "failed to stop core by service: {}", err);
|
||||
// 服务停止失败,尝试停止可能的sidecar进程
|
||||
self.stop_sidecar_process();
|
||||
@@ -79,22 +90,44 @@ impl CoreManager {
|
||||
}
|
||||
} else {
|
||||
// 如果没有使用服务,尝试停止sidecar进程
|
||||
println!("[停止内核] 服务不可用,尝试停止sidecar进程");
|
||||
self.stop_sidecar_process();
|
||||
}
|
||||
|
||||
// 释放文件锁
|
||||
println!("[停止内核] 尝试释放文件锁");
|
||||
if let Some(_) = handle::Handle::global().release_core_lock() {
|
||||
println!("[停止内核] 文件锁释放成功");
|
||||
log::info!(target: "app", "released core lock file");
|
||||
} else {
|
||||
println!("[停止内核] 没有文件锁需要释放");
|
||||
}
|
||||
|
||||
*running = false;
|
||||
println!("[停止内核] 内核停止完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止通过sidecar启动的进程
|
||||
fn stop_sidecar_process(&self) {
|
||||
if let Some(process) = handle::Handle::global().take_core_process() {
|
||||
println!("[停止sidecar] 发现sidecar进程,准备停止");
|
||||
log::info!(target: "app", "stopping core process in sidecar mode");
|
||||
|
||||
// 尝试获取进程ID
|
||||
let pid = process.pid();
|
||||
println!("[停止sidecar] 进程PID: {}", pid);
|
||||
|
||||
// 尝试终止进程
|
||||
if let Err(e) = process.kill() {
|
||||
println!("[停止sidecar] 终止sidecar进程失败: {}", e);
|
||||
log::warn!(target: "app", "failed to kill core process: {}", e);
|
||||
} else {
|
||||
println!("[停止sidecar] sidecar进程已成功终止");
|
||||
log::info!(target: "app", "core process stopped successfully");
|
||||
}
|
||||
} else {
|
||||
println!("[停止sidecar] 没有找到sidecar进程");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,15 +141,17 @@ impl CoreManager {
|
||||
|
||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||
|
||||
// 先尝试服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
// 先检查服务状态
|
||||
let service_available = service::check_service().await.is_ok();
|
||||
|
||||
if service_available {
|
||||
log::info!(target: "app", "try to run core in service mode");
|
||||
match service::run_core_by_service(&config_path).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core started successfully in service mode");
|
||||
}
|
||||
Err(err) => {
|
||||
// 服务启动失败,尝试sidecar模式
|
||||
// 服务启动失败,直接尝试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?;
|
||||
@@ -143,6 +178,99 @@ impl CoreManager {
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
|
||||
log::info!(target: "app", "starting core {} in sidecar mode", clash_core);
|
||||
println!("[sidecar启动] 开始以sidecar模式启动内核: {}", clash_core);
|
||||
|
||||
// 检查系统中是否存在同名进程
|
||||
if let Ok(pids) = self.check_existing_processes(&clash_core).await {
|
||||
if !pids.is_empty() {
|
||||
println!("[sidecar启动] 警告:系统中已存在同名进程");
|
||||
// 尝试检查端口占用
|
||||
if let Ok(config_content) = std::fs::read_to_string(config_path) {
|
||||
if let Ok(config) = serde_yaml::from_str::<serde_yaml::Value>(&config_content) {
|
||||
// 获取配置中定义的端口
|
||||
let mixed_port = config
|
||||
.get("mixed-port")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(7890);
|
||||
let http_port = config.get("port").and_then(|v| v.as_u64()).unwrap_or(7890);
|
||||
|
||||
println!(
|
||||
"[sidecar启动] 检查端口占用: HTTP端口={}, 混合端口={}",
|
||||
http_port, mixed_port
|
||||
);
|
||||
|
||||
// 检查端口是否被占用
|
||||
if self.is_port_in_use(mixed_port as u16).await
|
||||
|| self.is_port_in_use(http_port as u16).await
|
||||
{
|
||||
println!("[sidecar启动] 端口已被占用,尝试终止已存在的进程");
|
||||
|
||||
// 尝试终止已存在的进程
|
||||
for pid in pids {
|
||||
println!("[sidecar启动] 尝试终止进程 PID: {}", pid);
|
||||
self.terminate_process(pid).await;
|
||||
}
|
||||
|
||||
// 等待短暂时间让资源释放
|
||||
println!("[sidecar启动] 等待500ms让资源释放");
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("[sidecar启动] 无法检查系统进程,继续尝试启动");
|
||||
}
|
||||
|
||||
// 创建锁文件路径
|
||||
let lock_file = dirs::app_home_dir()?.join(format!("{}.lock", clash_core));
|
||||
println!("[sidecar启动] 锁文件路径: {:?}", lock_file);
|
||||
|
||||
// 尝试获取文件锁
|
||||
println!("[sidecar启动] 尝试获取文件锁");
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_file)?;
|
||||
|
||||
match file.try_lock_exclusive() {
|
||||
Ok(_) => {
|
||||
// 成功获取锁,说明没有其他实例运行
|
||||
println!("[sidecar启动] 成功获取文件锁,没有检测到其他运行的实例");
|
||||
log::info!(target: "app", "acquired lock for core process");
|
||||
|
||||
// 保存锁对象到全局,防止被Drop
|
||||
handle::Handle::global().set_core_lock(file);
|
||||
}
|
||||
Err(err) => {
|
||||
// 无法获取锁,说明已有实例运行
|
||||
println!("[sidecar启动] 无法获取文件锁,检测到其他实例可能正在运行");
|
||||
println!("[sidecar启动] 错误信息: {:?}", err);
|
||||
log::warn!(target: "app", "another core process appears to be running");
|
||||
|
||||
// 尝试强制获取锁(可能会导致其他进程崩溃)
|
||||
println!("[sidecar启动] 尝试强制删除并重新创建锁文件");
|
||||
std::fs::remove_file(&lock_file)?;
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_file)?;
|
||||
|
||||
println!("[sidecar启动] 尝试强制获取锁");
|
||||
match file.lock_exclusive() {
|
||||
Ok(_) => println!("[sidecar启动] 成功强制获取锁"),
|
||||
Err(e) => println!("[sidecar启动] 强制获取锁失败: {:?}", e),
|
||||
}
|
||||
file.lock_exclusive()?;
|
||||
|
||||
// 保存新锁
|
||||
handle::Handle::global().set_core_lock(file);
|
||||
|
||||
// 等待可能的其他进程退出
|
||||
println!("[sidecar启动] 等待500ms,让可能的其他进程退出");
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
@@ -153,6 +281,7 @@ impl CoreManager {
|
||||
let config_path_str = dirs::path_to_str(config_path)?;
|
||||
|
||||
// 启动核心进程并转入后台运行
|
||||
println!("[sidecar启动] 开始启动核心进程");
|
||||
let (_, child) = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)?
|
||||
@@ -160,11 +289,13 @@ impl CoreManager {
|
||||
.spawn()?;
|
||||
|
||||
// 保存进程ID以便后续管理
|
||||
println!("[sidecar启动] 核心进程启动成功,PID: {:?}", child.pid());
|
||||
handle::Handle::global().set_core_process(child);
|
||||
|
||||
// 等待短暂时间确保启动成功
|
||||
sleep(Duration::from_millis(300)).await;
|
||||
|
||||
println!("[sidecar启动] 内核启动完成");
|
||||
log::info!(target: "app", "core started in sidecar mode");
|
||||
Ok(())
|
||||
}
|
||||
@@ -179,6 +310,19 @@ impl CoreManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 强制重新安装服务(供UI调用,用户主动修复服务)
|
||||
pub async fn repair_service(&self) -> Result<()> {
|
||||
log::info!(target: "app", "user requested service repair");
|
||||
|
||||
// 调用强制重装服务
|
||||
service::force_reinstall_service().await?;
|
||||
|
||||
// 重启核心
|
||||
self.restart_core().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 使用默认配置
|
||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
@@ -538,6 +682,8 @@ impl CoreManager {
|
||||
// 5. 应用新配置
|
||||
println!("[core配置更新] 应用新配置");
|
||||
for i in 0..3 {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
|
||||
match MihomoManager::global().put_configs_force(run_path).await {
|
||||
Ok(_) => {
|
||||
println!("[core配置更新] 配置应用成功");
|
||||
@@ -618,7 +764,14 @@ impl CoreManager {
|
||||
_ => {
|
||||
// 服务存在但可能没有运行,检查是否有sidecar进程
|
||||
if handle::Handle::global().has_core_process() {
|
||||
RunningMode::Sidecar
|
||||
// 检查是否持有文件锁,确保是由我们启动的进程
|
||||
if handle::Handle::global().has_core_lock() {
|
||||
RunningMode::Sidecar
|
||||
} else {
|
||||
// 有进程但没有文件锁,可能是外部启动的进程
|
||||
log::warn!(target: "app", "core process exists but no lock file");
|
||||
RunningMode::Sidecar // 仍返回Sidecar模式,但记录了警告
|
||||
}
|
||||
} else {
|
||||
RunningMode::NotRunning
|
||||
}
|
||||
@@ -628,11 +781,226 @@ impl CoreManager {
|
||||
Err(_) => {
|
||||
// 服务不可用,检查是否有sidecar进程
|
||||
if handle::Handle::global().has_core_process() {
|
||||
RunningMode::Sidecar
|
||||
// 检查是否持有文件锁,确保是由我们启动的进程
|
||||
if handle::Handle::global().has_core_lock() {
|
||||
RunningMode::Sidecar
|
||||
} else {
|
||||
// 有进程但没有文件锁,可能是外部启动的进程
|
||||
log::warn!(target: "app", "core process exists but no lock file");
|
||||
RunningMode::Sidecar // 仍返回Sidecar模式,但记录了警告
|
||||
}
|
||||
} else {
|
||||
RunningMode::NotRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查系统中是否存在同名进程
|
||||
async fn check_existing_processes(&self, process_name: &str) -> Result<Vec<u32>> {
|
||||
println!("[进程检查] 检查系统中是否存在进程: {}", process_name);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] Windows系统,使用tasklist命令");
|
||||
let output = Command::new("tasklist")
|
||||
.args(["/FO", "CSV", "/NH"])
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter(|line| line.contains(process_name))
|
||||
.filter_map(|line| {
|
||||
println!("[进程检查] 发现匹配行: {}", line);
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() >= 2 {
|
||||
let pid_str = parts[1].trim_matches('"');
|
||||
pid_str.parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] Linux系统,使用pgrep命令");
|
||||
let output = Command::new("pgrep")
|
||||
.arg("-f")
|
||||
.arg(process_name)
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
line.trim().parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] macOS系统,使用ps命令");
|
||||
let output = Command::new("ps")
|
||||
.args(["-ax", "-o", "pid,command"])
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter(|line| line.contains(process_name))
|
||||
.filter_map(|line| {
|
||||
println!("[进程检查] 发现匹配行: {}", line);
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
parts[0].parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查端口是否被占用
|
||||
async fn is_port_in_use(&self, port: u16) -> bool {
|
||||
println!("[端口检查] 检查端口 {} 是否被占用", port);
|
||||
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
match TcpSocket::new_v4() {
|
||||
Ok(socket) => {
|
||||
let addr = format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
match socket.bind(addr) {
|
||||
Ok(_) => {
|
||||
// 如果能绑定成功,说明端口未被占用
|
||||
println!("[端口检查] 端口 {} 未被占用", port);
|
||||
false
|
||||
}
|
||||
Err(_) => {
|
||||
// 绑定失败,端口已被占用
|
||||
println!("[端口检查] 端口 {} 已被占用", port);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// 创建socket失败,保守返回端口被占用
|
||||
println!("[端口检查] 创建Socket失败: {:?}, 假设端口已被占用", err);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 终止进程
|
||||
async fn terminate_process(&self, pid: u32) {
|
||||
println!("[进程终止] 尝试终止进程 PID: {}", pid);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 确保 Mihomo 和 Verge service 都在运行
|
||||
pub async fn ensure_running_core(&self) {
|
||||
if MihomoManager::global().is_mihomo_running().await.is_err() {
|
||||
log_err!(self.restart_core().await);
|
||||
}
|
||||
match is_service_running().await {
|
||||
Ok(false) => log_err!(self.restart_core().await),
|
||||
Ok(true) => {
|
||||
if MihomoManager::global().is_mihomo_running().await.is_err() {
|
||||
log_err!(self.restart_core().await);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||
pub is_exiting: Arc<RwLock<bool>>,
|
||||
pub core_process: Arc<RwLock<Option<CommandChild>>>,
|
||||
pub core_lock: Arc<RwLock<Option<File>>>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
@@ -20,6 +22,7 @@ impl Handle {
|
||||
app_handle: Arc::new(RwLock::new(None)),
|
||||
is_exiting: Arc::new(RwLock::new(false)),
|
||||
core_process: Arc::new(RwLock::new(None)),
|
||||
core_lock: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,4 +92,21 @@ impl Handle {
|
||||
pub fn is_exiting(&self) -> bool {
|
||||
*self.is_exiting.read()
|
||||
}
|
||||
|
||||
/// 设置核心文件锁
|
||||
pub fn set_core_lock(&self, file: File) {
|
||||
let mut core_lock = self.core_lock.write();
|
||||
*core_lock = Some(file);
|
||||
}
|
||||
|
||||
/// 释放核心文件锁
|
||||
pub fn release_core_lock(&self) -> Option<File> {
|
||||
let mut core_lock = self.core_lock.write();
|
||||
core_lock.take()
|
||||
}
|
||||
|
||||
/// 检查是否持有核心文件锁
|
||||
pub fn has_core_lock(&self) -> bool {
|
||||
self.core_lock.read().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{config::Config, core::handle, feat, log_err, utils::resolve};
|
||||
use crate::{
|
||||
config::Config, core::handle, feat, log_err, module::lightweight::entry_lightweight_mode,
|
||||
utils::resolve,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
@@ -147,6 +150,7 @@ impl Hotkey {
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||
"quit" => || feat::quit(Some(0)),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => || feat::hide(),
|
||||
|
||||
@@ -1,13 +1,82 @@
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use anyhow::{bail, Context, Result};
|
||||
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;
|
||||
|
||||
// Windows only
|
||||
|
||||
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)]
|
||||
pub struct ResponseBody {
|
||||
@@ -41,6 +110,15 @@ pub struct VersionJsonResponse {
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
log::info!(target:"app", "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target:"app", "service reinstall rejected: cooldown period or max attempts reached");
|
||||
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||
}
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -74,12 +152,20 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
let error = format!(
|
||||
"failed to install service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.save()?;
|
||||
bail!(error);
|
||||
}
|
||||
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -226,19 +312,54 @@ pub async fn check_service_version() -> Result<String> {
|
||||
|
||||
/// check if service needs to be reinstalled
|
||||
pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 获取当前服务状态
|
||||
let service_state = ServiceState::get();
|
||||
|
||||
// 首先检查是否在冷却期或超过重装次数限制
|
||||
if !service_state.can_reinstall() {
|
||||
log::info!(target: "app", "service reinstall check: in cooldown period or max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 然后才检查版本和可用性
|
||||
match check_service_version().await {
|
||||
Ok(version) => version != REQUIRED_SERVICE_VERSION,
|
||||
Err(_) => true, // 如果无法获取版本或服务未运行,也需要重新安装
|
||||
Ok(version) => {
|
||||
// 打印更详细的日志,方便排查问题
|
||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
// 打印版本字符串的原始字节,确认没有隐藏字符
|
||||
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配,无需重装");
|
||||
}
|
||||
|
||||
needs_reinstall
|
||||
},
|
||||
Err(err) => {
|
||||
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
|
||||
match is_service_running().await {
|
||||
Ok(true) => {
|
||||
log::info!(target: "app", "service is running but version check failed: {}", err);
|
||||
false // 服务在运行,不需要重装
|
||||
}
|
||||
_ => {
|
||||
log::info!(target: "app", "service is not running or unavailable");
|
||||
true // 服务不可用,需要重装
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
// 检查服务版本,如果不匹配则重新安装
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "service version mismatch, reinstalling");
|
||||
reinstall_service().await?;
|
||||
}
|
||||
/// 尝试使用现有服务启动核心,不进行重装
|
||||
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target:"app", "attempting to start core with existing service");
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
@@ -250,6 +371,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
let config_dir = dirs::path_to_str(&config_dir)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
let config_dir = &(config_dir.replace("/verge-mihomo", "") + "/resources");
|
||||
|
||||
let log_path = dirs::service_log_file()?;
|
||||
let log_path = dirs::path_to_str(&log_path)?;
|
||||
@@ -278,6 +401,106 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
log::info!(target: "app", "正在尝试通过服务启动核心");
|
||||
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
// 通过字节比较确保完全匹配
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
false // 版本不匹配
|
||||
} else {
|
||||
log::info!(target: "app", "服务版本匹配");
|
||||
true // 版本匹配
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||
false // 无法获取版本
|
||||
}
|
||||
};
|
||||
|
||||
// 先尝试直接启动服务,如果服务可用且版本匹配
|
||||
if version_check {
|
||||
if let Ok(true) = is_service_running().await {
|
||||
// 服务正在运行且版本匹配,直接使用
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 强制执行版本检查,如果版本不匹配则重装
|
||||
if !version_check {
|
||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||
|
||||
// 获取服务状态,检查是否可以重装
|
||||
let service_state = ServiceState::get();
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target: "app", "由于限制无法重装服务");
|
||||
// 尝试直接启动,即使版本不匹配
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试重装
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
|
||||
// 尝试使用现有服务
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 重装成功,尝试启动
|
||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
match check_service().await {
|
||||
Ok(_) => {
|
||||
// 服务可访问但可能没有运行核心,尝试直接启动
|
||||
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务不可用或启动失败,检查是否需要重装
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
|
||||
// 尝试重装
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
// 重装后再次尝试启动
|
||||
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
||||
start_with_existing_service(config_file).await
|
||||
} else {
|
||||
// 不需要或不能重装,返回错误
|
||||
log::warn!(target: "app", "服务不可用且无法重装");
|
||||
bail!("Service is not available and cannot be reinstalled at this time")
|
||||
}
|
||||
}
|
||||
|
||||
/// stop the clash by service
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
let url = format!("{SERVICE_URL}/stop_clash");
|
||||
@@ -303,3 +526,26 @@ pub async fn is_service_running() -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 强制重装服务(用于UI中的修复服务按钮)
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
|
||||
// 创建默认服务状态(重置所有限制)
|
||||
let service_state = ServiceState::default();
|
||||
service_state.save()?;
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
|
||||
// 执行重装
|
||||
match reinstall_service().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat,
|
||||
module::mihomo::Rate,
|
||||
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
|
||||
resolve,
|
||||
utils::{dirs, i18n::t, resolve::VERSION},
|
||||
};
|
||||
@@ -21,6 +21,10 @@ use parking_lot::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::hash::{Hash, Hasher};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use tauri::{
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
@@ -36,6 +40,9 @@ pub struct Tray {
|
||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub icon_hash: Arc<Mutex<Option<u64>>>,
|
||||
pub icon_cache: Arc<Mutex<Option<Vec<u8>>>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -50,6 +57,9 @@ impl Tray {
|
||||
speed_rate: Arc::new(Mutex::new(None)),
|
||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||
is_subscribed: Arc::new(RwLock::new(false)),
|
||||
icon_hash: Arc::new(Mutex::new(None)),
|
||||
icon_cache: Arc::new(Mutex::new(None)),
|
||||
rate_cache: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -84,6 +94,7 @@ impl Tray {
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app","tray event: {:?}", tray_event);
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
@@ -228,34 +239,66 @@ impl Tray {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
|
||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||
let is_colorful = tray_icon == "colorful";
|
||||
|
||||
// 处理图标和速率
|
||||
let final_icon_bytes = if enable_tray_speed {
|
||||
let rate = rate.or_else(|| {
|
||||
self.speed_rate
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
||||
});
|
||||
|
||||
// 使用新的方法渲染图标和速率
|
||||
SpeedRate::add_speed_text(icon_bytes, rate)?
|
||||
} else {
|
||||
icon_bytes
|
||||
let icon_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
icon_bytes.clone().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
// 设置系统托盘图标
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
||||
// 只对单色图标使用 template 模式
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
let mut icon_hash_guard = self.icon_hash.lock();
|
||||
let mut icon_bytes_guard = self.icon_cache.lock();
|
||||
if *icon_hash_guard != Some(icon_hash) {
|
||||
*icon_hash_guard = Some(icon_hash);
|
||||
*icon_bytes_guard = Some(icon_bytes.clone());
|
||||
}
|
||||
|
||||
if !enable_tray_speed || (!enable_tray_speed && !enable_tray_icon) {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(
|
||||
&(*icon_bytes_guard).clone().unwrap(),
|
||||
)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(rate) = guard.as_ref().unwrap().get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = if enable_tray_icon {
|
||||
Some(icon_bytes_guard.as_ref().unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rate = rate_guard.as_ref();
|
||||
let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap();
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
|
||||
Ok(())
|
||||
{
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
@@ -490,6 +533,15 @@ fn create_tray_menu(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let lighteweight_mode = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"entry_lightweight_mode",
|
||||
t("LightWeight Mode"),
|
||||
true,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_env =
|
||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||
|
||||
@@ -582,6 +634,8 @@ fn create_tray_menu(
|
||||
separator,
|
||||
system_proxy,
|
||||
tun_mode,
|
||||
separator,
|
||||
lighteweight_mode,
|
||||
copy_env,
|
||||
open_dir,
|
||||
more,
|
||||
@@ -609,6 +663,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||
"quit" => {
|
||||
println!("quit");
|
||||
feat::quit(Some(0));
|
||||
|
||||
@@ -15,7 +15,6 @@ use tungstenite::client::IntoClientRequest;
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
// 移除 base_image,不再缓存原始图像
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
@@ -77,29 +76,51 @@ impl SpeedRate {
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
||||
pub fn add_speed_text<'a>(
|
||||
icon_bytes: Option<&'a Vec<u8>>,
|
||||
rate: Option<&'a Rate>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||
|
||||
// 加载原始图标
|
||||
let icon_image = image::load_from_memory(&icon_bytes)?;
|
||||
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
||||
let (mut icon_width, mut icon_height) = (0, 256);
|
||||
let icon_image = if let Some(bytes) = icon_bytes {
|
||||
let icon_image = image::load_from_memory(bytes)?;
|
||||
icon_width = icon_image.width();
|
||||
icon_height = icon_image.height();
|
||||
icon_image
|
||||
} else {
|
||||
// 返回一个空的 RGBA 图像
|
||||
image::DynamicImage::new_rgba8(0, 0)
|
||||
};
|
||||
|
||||
// 判断是否为彩色图标
|
||||
let is_colorful =
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||
let is_colorful = if let Some(bytes) = icon_bytes {
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 增加文本宽度和间距
|
||||
let text_width = 580; // 文本区域宽度
|
||||
let total_width = icon_width + text_width;
|
||||
let total_width = if icon_bytes.is_some() {
|
||||
if icon_width < 580 {
|
||||
icon_width + 580
|
||||
} else {
|
||||
icon_width
|
||||
}
|
||||
} else {
|
||||
580
|
||||
};
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
||||
// 将原始图标绘制到新画布的左侧
|
||||
for y in 0..icon_height {
|
||||
for x in 0..icon_width {
|
||||
let pixel = icon_image.get_pixel(x, y);
|
||||
combined_image.put_pixel(x, y, pixel);
|
||||
if icon_bytes.is_some() {
|
||||
for y in 0..icon_height {
|
||||
for x in 0..icon_width {
|
||||
let pixel = icon_image.get_pixel(x, y);
|
||||
combined_image.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ pub fn change_clash_mode(mode: String) {
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
|
||||
match MihomoManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
@@ -66,6 +67,7 @@ pub fn change_clash_mode(mode: String) {
|
||||
|
||||
/// Test connection delay to a URL
|
||||
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
|
||||
@@ -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_port = patch.verge_port;
|
||||
let enable_tray_speed = patch.enable_tray_speed;
|
||||
let enable_tray_icon = patch.enable_tray_icon;
|
||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||
let tray_event = patch.tray_event;
|
||||
let home_cards = patch.home_cards.clone();
|
||||
@@ -145,6 +146,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
|| tun_tray_icon.is_some()
|
||||
|| tray_icon.is_some()
|
||||
|| enable_tray_speed.is_some()
|
||||
|| enable_tray_icon.is_some()
|
||||
{
|
||||
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||
}
|
||||
@@ -164,6 +166,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
|
||||
@@ -11,12 +11,9 @@ use crate::{
|
||||
};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem, Submenu},
|
||||
AppHandle,
|
||||
};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
@@ -155,6 +152,7 @@ pub fn run() {
|
||||
// 添加新的命令
|
||||
cmd::get_running_mode,
|
||||
cmd::install_service,
|
||||
cmd::repair_service,
|
||||
cmd::get_app_uptime,
|
||||
cmd::get_auto_launch_status,
|
||||
// clash
|
||||
@@ -227,18 +225,7 @@ pub fn run() {
|
||||
// Macos Application Menu
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.menu(|handle| {
|
||||
Menu::with_items(
|
||||
handle,
|
||||
&[&Submenu::with_items(
|
||||
handle,
|
||||
"Menu",
|
||||
true,
|
||||
&[&MenuItem::new(handle, "Clash Verge", true, None::<String>).unwrap()],
|
||||
)
|
||||
.unwrap()],
|
||||
)
|
||||
});
|
||||
// Temporary Achived due to cannot CMD+C/V/A
|
||||
}
|
||||
|
||||
let app = builder
|
||||
@@ -250,11 +237,12 @@ pub fn run() {
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let main_window = AppHandleManager::global()
|
||||
if let Some(window) = AppHandleManager::global()
|
||||
.get_handle()
|
||||
.get_webview_window("main")
|
||||
.unwrap();
|
||||
let _ = main_window.set_title("Clash Verge");
|
||||
{
|
||||
let _ = window.set_title("Clash Verge");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -5,7 +5,7 @@ use tauri::{Listener, Manager};
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer},
|
||||
log_err,
|
||||
log_err, AppHandleManager,
|
||||
};
|
||||
|
||||
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() {
|
||||
println!("尝试进入轻量模式。motherfucker");
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
log_err!(window.close());
|
||||
}
|
||||
|
||||
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 window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
}
|
||||
if let Some(webview) = window.get_webview_window("main") {
|
||||
let _ = webview.destroy();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
println!("[lightweight_mode] 轻量模式已开启");
|
||||
log::info!(target: "app", "[lightweight_mode] 轻量模式已开启");
|
||||
}
|
||||
let _ = cancel_light_weight_timer();
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() -> u32 {
|
||||
@@ -132,10 +133,9 @@ fn cancel_light_weight_timer() -> Result<()> {
|
||||
delay_timer
|
||||
.remove_task(task.task_id)
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -98,6 +98,12 @@ 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> {
|
||||
let url = format!("{}/configs?force=true", self.mihomo_server);
|
||||
let payload = serde_json::json!({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
@@ -30,12 +30,6 @@
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||
"endpoints": [
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
|
||||
],
|
||||
"windows": {
|
||||
"installMode": "basicUi"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { t } from "i18next";
|
||||
@@ -14,6 +14,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
const theme = useTheme();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
@@ -35,6 +36,8 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
maxWidth: "520px",
|
||||
maxHeight: "480px",
|
||||
overflowY: "auto",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
message={
|
||||
@@ -54,6 +57,7 @@ interface InnerProps {
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const theme = useTheme();
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
@@ -99,11 +103,11 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>
|
||||
<span style={{ wordBreak: "break-all" }}>: {each.value}</span>
|
||||
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useClash } from "@/hooks/use-clash";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import useSWR from "swr";
|
||||
import { getRules } from "@/services/api";
|
||||
import { getAppUptime } from "@/services/cmds";
|
||||
import { useMemo } from "react";
|
||||
import { getAppUptime, getSystemProxy } from "@/services/cmds";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
// 将毫秒转换为时:分:秒格式的函数
|
||||
const formatUptime = (uptimeMs: number) => {
|
||||
@@ -21,6 +21,8 @@ export const ClashInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { version: clashVersion } = useClash();
|
||||
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
|
||||
const [rules, setRules] = useState<any[]>([]);
|
||||
|
||||
// 使用SWR获取应用运行时间,降低更新频率
|
||||
const { data: uptimeMs = 0 } = useSWR(
|
||||
@@ -33,15 +35,18 @@ export const ClashInfoCard = () => {
|
||||
},
|
||||
);
|
||||
|
||||
// 在组件加载时获取系统代理信息和规则数据
|
||||
useEffect(() => {
|
||||
// 获取系统代理信息
|
||||
getSystemProxy().then(setSysproxy);
|
||||
|
||||
// 获取规则数据
|
||||
getRules().then(setRules).catch(() => setRules([]));
|
||||
}, []);
|
||||
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
||||
|
||||
// 获取规则数据,只在组件加载时获取一次
|
||||
const { data: rules = [] } = useSWR("getRules", getRules, {
|
||||
revalidateOnFocus: false,
|
||||
errorRetryCount: 2,
|
||||
});
|
||||
|
||||
// 使用备忘录组件内容,减少重新渲染
|
||||
const cardContent = useMemo(() => {
|
||||
if (!clashInfo) return null;
|
||||
@@ -62,7 +67,7 @@ export const ClashInfoCard = () => {
|
||||
{t("System Proxy Address")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.server || "-"}
|
||||
{sysproxy?.server || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
@@ -94,7 +99,7 @@ export const ClashInfoCard = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length]);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MultipleStopRounded,
|
||||
DirectionsRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const ClashModeCard = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,21 +20,19 @@ export const ClashModeCard = () => {
|
||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig,
|
||||
{ revalidateOnFocus: false }
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
dedupingInterval: 1000,
|
||||
errorRetryInterval: 5000
|
||||
}
|
||||
);
|
||||
|
||||
// 支持的模式列表
|
||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||
|
||||
// 本地状态记录当前模式
|
||||
const [localMode, setLocalMode] = useState<string>("rule");
|
||||
|
||||
// 当从API获取到当前模式时更新本地状态
|
||||
useEffect(() => {
|
||||
if (clashConfig?.mode) {
|
||||
setLocalMode(clashConfig.mode.toLowerCase());
|
||||
}
|
||||
}, [clashConfig]);
|
||||
// 直接使用API返回的模式,不维护本地状态
|
||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
// 模式图标映射
|
||||
const modeIcons = useMemo(() => ({
|
||||
@@ -45,10 +43,7 @@ export const ClashModeCard = () => {
|
||||
|
||||
// 切换模式的处理函数
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
if (mode === localMode) return;
|
||||
|
||||
setLocalMode(mode);
|
||||
|
||||
if (mode === currentMode) return;
|
||||
if (verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
@@ -58,9 +53,6 @@ export const ClashModeCard = () => {
|
||||
mutateClash();
|
||||
} catch (error) {
|
||||
console.error("Failed to change mode:", error);
|
||||
if (clashConfig?.mode) {
|
||||
setLocalMode(clashConfig.mode.toLowerCase());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,8 +65,8 @@ export const ClashModeCard = () => {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: mode === localMode ? "primary.main" : "background.paper",
|
||||
color: mode === localMode ? "primary.contrastText" : "text.primary",
|
||||
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
|
||||
color: mode === currentMode ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
@@ -86,7 +78,7 @@ export const ClashModeCard = () => {
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after": mode === localMode
|
||||
"&::after": mode === currentMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
@@ -132,7 +124,7 @@ export const ClashModeCard = () => {
|
||||
{modeList.map((mode) => (
|
||||
<Paper
|
||||
key={mode}
|
||||
elevation={mode === localMode ? 2 : 0}
|
||||
elevation={mode === currentMode ? 2 : 0}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={buttonStyles(mode)}
|
||||
>
|
||||
@@ -141,7 +133,7 @@ export const ClashModeCard = () => {
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontWeight: mode === localMode ? 600 : 400,
|
||||
fontWeight: mode === currentMode ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{t(mode)}
|
||||
@@ -161,15 +153,13 @@ export const ClashModeCard = () => {
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={descriptionStyles}
|
||||
>
|
||||
{t(`${localMode} Mode Description`)}
|
||||
</Typography>
|
||||
</Fade>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={descriptionStyles}
|
||||
>
|
||||
{t(`${currentMode} Mode Description`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -53,11 +53,16 @@ function convertDelayColor(delayValue: number) {
|
||||
const mainColor = colorStr.split(".")[0];
|
||||
|
||||
switch (mainColor) {
|
||||
case "success": return "success";
|
||||
case "warning": return "warning";
|
||||
case "error": return "error";
|
||||
case "primary": return "primary";
|
||||
default: return "default";
|
||||
case "success":
|
||||
return "success";
|
||||
case "warning":
|
||||
return "warning";
|
||||
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) {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return function(this: any, ...args: any[]) {
|
||||
return function (this: any, ...args: any[]) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
@@ -87,7 +92,8 @@ function debounce(fn: Function, ms = 100) {
|
||||
|
||||
export const CurrentProxyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentProxy, primaryGroupName, mode, refreshProxy } = useCurrentProxy();
|
||||
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
||||
useCurrentProxy();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { verge } = useVerge();
|
||||
@@ -135,147 +141,150 @@ export const CurrentProxyCard = () => {
|
||||
useEffect(() => {
|
||||
// 根据模式确定初始组
|
||||
if (isGlobalMode) {
|
||||
setState(prev => ({
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: "GLOBAL"
|
||||
}
|
||||
group: "GLOBAL",
|
||||
},
|
||||
}));
|
||||
} else if (isDirectMode) {
|
||||
setState(prev => ({
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: "DIRECT"
|
||||
}
|
||||
group: "DIRECT",
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||
setState(prev => ({
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: savedGroup || primaryGroupName || ""
|
||||
}
|
||||
group: savedGroup || primaryGroupName || "",
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
||||
|
||||
// 带锁的代理数据获取函数,防止并发请求
|
||||
const fetchProxyData = useCallback(async (force = false) => {
|
||||
// 防止重复请求
|
||||
if (isRefreshingRef.current) {
|
||||
pendingRefreshRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查刷新间隔
|
||||
const now = Date.now();
|
||||
if (!force && now - lastRefreshRef.current < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRefreshingRef.current = true;
|
||||
lastRefreshRef.current = now;
|
||||
|
||||
try {
|
||||
const data = await getProxies();
|
||||
|
||||
// 过滤和格式化组
|
||||
const filteredGroups = data.groups
|
||||
.filter(g => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||
.map(g => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map(p => p.name),
|
||||
}));
|
||||
|
||||
// 使用函数式更新确保状态更新的原子性
|
||||
setState(prev => {
|
||||
let newProxy = "";
|
||||
let newDisplayProxy = null;
|
||||
let newGroup = prev.selection.group;
|
||||
|
||||
// 根据模式确定新代理
|
||||
if (isDirectMode) {
|
||||
newGroup = "DIRECT";
|
||||
newProxy = "DIRECT";
|
||||
newDisplayProxy = data.records?.DIRECT || null;
|
||||
} else if (isGlobalMode && data.global) {
|
||||
newGroup = "GLOBAL";
|
||||
newProxy = data.global.now || "";
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
} else {
|
||||
// 普通模式 - 检查当前选择的组是否存在
|
||||
const currentGroup = filteredGroups.find(g => g.name === prev.selection.group);
|
||||
|
||||
// 如果当前组不存在或为空,自动选择第一个组
|
||||
if (!currentGroup && filteredGroups.length > 0) {
|
||||
newGroup = filteredGroups[0].name;
|
||||
const firstGroup = filteredGroups[0];
|
||||
newProxy = firstGroup.now;
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
|
||||
// 保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
}
|
||||
} else if (currentGroup) {
|
||||
// 使用当前组的代理
|
||||
newProxy = currentGroup.now;
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回新状态
|
||||
return {
|
||||
proxyData: {
|
||||
groups: filteredGroups,
|
||||
records: data.records || {},
|
||||
globalProxy: data.global?.now || "",
|
||||
directProxy: data.records?.DIRECT || null,
|
||||
},
|
||||
selection: {
|
||||
group: newGroup,
|
||||
proxy: newProxy
|
||||
},
|
||||
displayProxy: newDisplayProxy
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取代理信息失败", error);
|
||||
} finally {
|
||||
isRefreshingRef.current = false;
|
||||
|
||||
// 处理待处理的刷新请求
|
||||
if (pendingRefreshRef.current) {
|
||||
pendingRefreshRef.current = false;
|
||||
setTimeout(() => fetchProxyData(), 100);
|
||||
const fetchProxyData = useCallback(
|
||||
async (force = false) => {
|
||||
// 防止重复请求
|
||||
if (isRefreshingRef.current) {
|
||||
pendingRefreshRef.current = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [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 变化
|
||||
useEffect(() => {
|
||||
if (currentProxy && (!state.displayProxy || currentProxy.name !== state.displayProxy.name)) {
|
||||
if (
|
||||
currentProxy &&
|
||||
(!state.displayProxy || currentProxy.name !== state.displayProxy.name)
|
||||
) {
|
||||
fetchProxyData(true);
|
||||
}
|
||||
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
||||
|
||||
// 平滑的定期刷新,使用固定间隔
|
||||
// 监听模式变化,mode变化时刷新
|
||||
useEffect(() => {
|
||||
fetchProxyData();
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
fetchProxyData();
|
||||
}, 3000); // 使用固定的3秒间隔,平衡响应速度和性能
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchProxyData]);
|
||||
fetchProxyData(true);
|
||||
}, [mode, fetchProxyData]);
|
||||
|
||||
|
||||
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||
const proxyOptions = useMemo(() => {
|
||||
@@ -285,14 +294,16 @@ export const CurrentProxyCard = () => {
|
||||
if (isGlobalMode && state.proxyData.records) {
|
||||
// 全局模式下的选项
|
||||
return Object.keys(state.proxyData.records)
|
||||
.filter(name => name !== "DIRECT" && name !== "REJECT")
|
||||
.map(name => ({ name }));
|
||||
.filter((name) => name !== "DIRECT" && name !== "REJECT")
|
||||
.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) {
|
||||
return group.all.map(name => ({ name }));
|
||||
return group.all.map((name) => ({ name }));
|
||||
}
|
||||
return [];
|
||||
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
||||
@@ -302,88 +313,103 @@ export const CurrentProxyCard = () => {
|
||||
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
||||
setState(updateFn);
|
||||
}, 50),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
// 处理代理组变更
|
||||
const handleGroupChange = useCallback((event: SelectChangeEvent) => {
|
||||
if (isGlobalMode || isDirectMode) return;
|
||||
|
||||
const newGroup = event.target.value;
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
|
||||
// 获取该组当前选中的代理
|
||||
setState(prev => {
|
||||
const group = prev.proxyData.groups.find(g => g.name === newGroup);
|
||||
if (group) {
|
||||
const handleGroupChange = useCallback(
|
||||
(event: SelectChangeEvent) => {
|
||||
if (isGlobalMode || isDirectMode) return;
|
||||
|
||||
const newGroup = event.target.value;
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
|
||||
// 获取该组当前选中的代理
|
||||
setState((prev) => {
|
||||
const group = prev.proxyData.groups.find((g) => g.name === newGroup);
|
||||
if (group) {
|
||||
return {
|
||||
...prev,
|
||||
selection: {
|
||||
group: newGroup,
|
||||
proxy: group.now,
|
||||
},
|
||||
displayProxy: prev.proxyData.records[group.now] || null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: newGroup,
|
||||
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,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: newGroup
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [isGlobalMode, isDirectMode]);
|
||||
proxy: newProxy,
|
||||
},
|
||||
displayProxy: prev.proxyData.records[newProxy] || null,
|
||||
}));
|
||||
|
||||
// 处理代理节点变更
|
||||
const handleProxyChange = useCallback(async (event: SelectChangeEvent) => {
|
||||
if (isDirectMode) return;
|
||||
|
||||
const newProxy = event.target.value;
|
||||
const currentGroup = state.selection.group;
|
||||
const previousProxy = state.selection.proxy;
|
||||
|
||||
// 立即更新UI,优化体验
|
||||
debouncedSetState((prev: ProxyState) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
proxy: newProxy
|
||||
},
|
||||
displayProxy: prev.proxyData.records[newProxy] || null
|
||||
}));
|
||||
|
||||
// 非特殊模式下保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新代理设置
|
||||
await updateProxy(currentGroup, newProxy);
|
||||
|
||||
// 自动关闭连接设置
|
||||
if (verge?.auto_close_connection && previousProxy) {
|
||||
getConnections().then(({ connections }) => {
|
||||
connections.forEach(conn => {
|
||||
if (conn.chains.includes(previousProxy)) {
|
||||
deleteConnection(conn.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
// 非特殊模式下保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
|
||||
// 刷新代理信息,使用较短的延迟
|
||||
setTimeout(() => {
|
||||
refreshProxy();
|
||||
fetchProxyData(true);
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error("更新代理失败", error);
|
||||
}
|
||||
}, [isDirectMode, isGlobalMode, state.proxyData.records, state.selection, verge?.auto_close_connection, refreshProxy, fetchProxyData, debouncedSetState]);
|
||||
|
||||
try {
|
||||
// 更新代理设置
|
||||
await updateProxy(currentGroup, newProxy);
|
||||
|
||||
// 自动关闭连接设置
|
||||
if (verge?.auto_close_connection && previousProxy) {
|
||||
getConnections().then(({ connections }) => {
|
||||
connections.forEach((conn) => {
|
||||
if (conn.chains.includes(previousProxy)) {
|
||||
deleteConnection(conn.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新代理信息,使用较短的延迟
|
||||
setTimeout(() => {
|
||||
refreshProxy();
|
||||
fetchProxyData(true);
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error("更新代理失败", error);
|
||||
}
|
||||
},
|
||||
[
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
state.proxyData.records,
|
||||
state.selection,
|
||||
verge?.auto_close_connection,
|
||||
refreshProxy,
|
||||
fetchProxyData,
|
||||
debouncedSetState,
|
||||
],
|
||||
);
|
||||
|
||||
// 导航到代理页面
|
||||
const goToProxies = useCallback(() => {
|
||||
@@ -392,35 +418,38 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
// 获取要显示的代理节点
|
||||
const proxyToDisplay = state.displayProxy || currentProxy;
|
||||
|
||||
|
||||
// 获取当前节点的延迟
|
||||
const currentDelay = proxyToDisplay
|
||||
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
||||
: -1;
|
||||
|
||||
|
||||
// 获取信号图标
|
||||
const signalInfo = getSignalIcon(currentDelay);
|
||||
|
||||
// 自定义渲染选择框中的值
|
||||
const renderProxyValue = useCallback((selected: string) => {
|
||||
if (!selected || !state.proxyData.records[selected]) return selected;
|
||||
const renderProxyValue = useCallback(
|
||||
(selected: string) => {
|
||||
if (!selected || !state.proxyData.records[selected]) return selected;
|
||||
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[selected],
|
||||
state.selection.group
|
||||
);
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[selected],
|
||||
state.selection.group,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography noWrap>{selected}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delayValue)}
|
||||
color={convertDelayColor(delayValue)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}, [state.proxyData.records, state.selection.group]);
|
||||
return (
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography noWrap>{selected}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delayValue)}
|
||||
color={convertDelayColor(delayValue)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
[state.proxyData.records, state.selection.group],
|
||||
);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
@@ -471,22 +500,48 @@ export const CurrentProxyCard = () => {
|
||||
{proxyToDisplay.name}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{proxyToDisplay.type}
|
||||
</Typography>
|
||||
{isGlobalMode && (
|
||||
<Chip size="small" label={t("Global Mode")} color="primary" sx={{ mr: 0.5 }} />
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Global Mode")}
|
||||
color="primary"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
{isDirectMode && (
|
||||
<Chip size="small" label={t("Direct Mode")} color="success" sx={{ mr: 0.5 }} />
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Direct Mode")}
|
||||
color="success"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
{/* 节点特性 */}
|
||||
{proxyToDisplay.udp && <Chip size="small" label="UDP" variant="outlined" />}
|
||||
{proxyToDisplay.tfo && <Chip size="small" label="TFO" variant="outlined" />}
|
||||
{proxyToDisplay.xudp && <Chip size="small" label="XUDP" variant="outlined" />}
|
||||
{proxyToDisplay.mptcp && <Chip size="small" label="MPTCP" variant="outlined" />}
|
||||
{proxyToDisplay.smux && <Chip size="small" label="SMUX" variant="outlined" />}
|
||||
{proxyToDisplay.udp && (
|
||||
<Chip size="small" label="UDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.tfo && (
|
||||
<Chip size="small" label="TFO" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.xudp && (
|
||||
<Chip size="small" label="XUDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.mptcp && (
|
||||
<Chip size="small" label="MPTCP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.smux && (
|
||||
<Chip size="small" label="SMUX" variant="outlined" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -500,7 +555,12 @@ export const CurrentProxyCard = () => {
|
||||
)}
|
||||
</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>
|
||||
<Select
|
||||
labelId="proxy-group-select-label"
|
||||
@@ -535,39 +595,41 @@ export const CurrentProxyCard = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{proxyOptions.map((proxy) => {
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group
|
||||
);
|
||||
return (
|
||||
<MenuItem
|
||||
key={proxy.name}
|
||||
value={proxy.name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{proxy.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delayValue)}
|
||||
color={convertDelayColor(delayValue)}
|
||||
sx={{
|
||||
minWidth: "60px",
|
||||
height: "22px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
{isDirectMode
|
||||
? null
|
||||
: proxyOptions.map((proxy) => {
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group,
|
||||
);
|
||||
return (
|
||||
<MenuItem
|
||||
key={proxy.name}
|
||||
value={proxy.name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{proxy.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delayValue)}
|
||||
color={convertDelayColor(delayValue)}
|
||||
sx={{
|
||||
minWidth: "60px",
|
||||
height: "22px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
@@ -31,6 +31,16 @@ export const EnhancedCard = ({
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
|
||||
// 统一的标题截断样式
|
||||
const titleTruncateStyle = {
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block"
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -52,7 +62,13 @@ export const EnhancedCard = ({
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -62,21 +78,32 @@ export const EnhancedCard = ({
|
||||
width: 38,
|
||||
height: 38,
|
||||
mr: 1.5,
|
||||
flexShrink: 0,
|
||||
backgroundColor: alpha(theme.palette[iconColor].main, 0.12),
|
||||
color: theme.palette[iconColor].main,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
{typeof title === "string" ? (
|
||||
<Typography variant="h6" fontWeight="medium" fontSize={18}>
|
||||
{title}
|
||||
</Typography>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
{typeof title === "string" ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
fontSize={18}
|
||||
sx={titleTruncateStyle}
|
||||
title={title}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={titleTruncateStyle}>
|
||||
{title}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{action}
|
||||
{action && <Box sx={{ ml: 2, flexShrink: 0 }}>{action}</Box>}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -35,19 +35,10 @@ const round = keyframes`
|
||||
`;
|
||||
|
||||
// 辅助函数解析URL和过期时间
|
||||
const parseUrl = (url?: string, maxLength: number = 25) => {
|
||||
const parseUrl = (url?: string) => {
|
||||
if (!url) return "-";
|
||||
let parsedUrl = "";
|
||||
if (url.startsWith("http")) {
|
||||
parsedUrl = new URL(url).host;
|
||||
} else {
|
||||
parsedUrl = "local";
|
||||
}
|
||||
|
||||
if (parsedUrl.length > maxLength) {
|
||||
return parsedUrl.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
return parsedUrl;
|
||||
if (url.startsWith("http")) return new URL(url).host;
|
||||
return "local";
|
||||
};
|
||||
|
||||
const parseExpire = (expire?: number) => {
|
||||
@@ -81,6 +72,14 @@ export interface HomeProfileCardProps {
|
||||
onProfileUpdated?: () => void;
|
||||
}
|
||||
|
||||
// 添加一个通用的截断样式
|
||||
const truncateStyle = {
|
||||
maxWidth: "calc(100% - 28px)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
};
|
||||
|
||||
// 提取独立组件减少主组件复杂度
|
||||
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
current: ProfileItem;
|
||||
@@ -109,31 +108,55 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
{current.url && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<DnsOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("From")}:{" "}
|
||||
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}>
|
||||
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
fontWeight="medium"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center"
|
||||
display: "inline-flex",
|
||||
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
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Box
|
||||
component="span"
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight="medium"
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
ml: 0.5
|
||||
}}
|
||||
title={parseUrl(current.url)}
|
||||
>
|
||||
{parseUrl(current.url)}
|
||||
</Box>
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -285,16 +308,30 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
fontSize={18}
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
color: "inherit",
|
||||
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
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ export const IpInfoCard = () => {
|
||||
}
|
||||
>
|
||||
<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="70%" height={24} />
|
||||
<Skeleton variant="text" width="50%" height={24} />
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getAutotemProxy,
|
||||
getRunningMode,
|
||||
} from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
|
||||
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: runningMode } = useSWR("getRunningMode", getRunningMode);
|
||||
const { verge } = useVerge();
|
||||
|
||||
// 从verge配置中获取开关状态
|
||||
const { enable_system_proxy, enable_tun_mode } = verge ?? {};
|
||||
|
||||
// 是否以sidecar模式运行
|
||||
const isSidecarMode = runningMode === "sidecar";
|
||||
@@ -170,7 +175,7 @@ export const ProxyTunCard: FC = () => {
|
||||
const tabDescription = useMemo(() => {
|
||||
if (activeTab === "system") {
|
||||
return {
|
||||
text: sysproxy?.enable
|
||||
text: enable_system_proxy
|
||||
? t("System Proxy Enabled")
|
||||
: t("System Proxy Disabled"),
|
||||
tooltip: t("System Proxy Info")
|
||||
@@ -179,11 +184,13 @@ export const ProxyTunCard: FC = () => {
|
||||
return {
|
||||
text: isSidecarMode
|
||||
? t("TUN Mode Service Required")
|
||||
: t("TUN Mode Intercept Info"),
|
||||
tooltip: t("Tun Mode Info")
|
||||
: enable_tun_mode
|
||||
? 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 (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
@@ -203,13 +210,14 @@ export const ProxyTunCard: FC = () => {
|
||||
onClick={() => handleTabChange("system")}
|
||||
icon={ComputerRounded}
|
||||
label={t("System Proxy")}
|
||||
hasIndicator={sysproxy?.enable}
|
||||
hasIndicator={enable_system_proxy}
|
||||
/>
|
||||
<TabButton
|
||||
isActive={activeTab === "tun"}
|
||||
onClick={() => handleTabChange("tun")}
|
||||
icon={TroubleshootRounded}
|
||||
label={t("Tun Mode")}
|
||||
hasIndicator={enable_tun_mode && !isSidecarMode}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const HOTKEY_FUNC = [
|
||||
"clash_mode_direct",
|
||||
"toggle_system_proxy",
|
||||
"toggle_tun_mode",
|
||||
"entry_lightweight_mode",
|
||||
];
|
||||
|
||||
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
@@ -198,6 +198,26 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</GuardState>
|
||||
</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>
|
||||
<ListItemText primary={t("Common Tray Icon")} />
|
||||
|
||||
@@ -66,9 +66,9 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "primary.main",
|
||||
"&:hover": { textDecoration: "underline" }
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
onClick={() => entry_lightweight_mode()}
|
||||
onClick={async () => await entry_lightweight_mode()}
|
||||
>
|
||||
{t("Enable")}
|
||||
</Typography>
|
||||
@@ -115,17 +115,25 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{t("mins")}</InputAdornment>
|
||||
)
|
||||
}
|
||||
<InputAdornment position="end">
|
||||
{t("mins")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: "italic" }}>
|
||||
{t("When closing the window, LightWeight Mode will be automatically activated after _n minutes",
|
||||
{ n: values.autoEnterLiteModeDelay })}
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: "italic" }}
|
||||
>
|
||||
{t(
|
||||
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
|
||||
{ n: values.autoEnterLiteModeDelay },
|
||||
)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</>
|
||||
@@ -133,4 +141,4 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,17 +120,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{proxy_auto_config ? (
|
||||
autoproxy?.enable ? (
|
||||
<PlayCircleOutlineRounded
|
||||
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
||||
/>
|
||||
) : (
|
||||
<PauseCircleOutlineRounded
|
||||
sx={{ color: "text.disabled", mr: 1.5, fontSize: 28 }}
|
||||
/>
|
||||
)
|
||||
) : sysproxy?.enable ? (
|
||||
{enable_system_proxy ? (
|
||||
<PlayCircleOutlineRounded
|
||||
sx={{ color: "success.main", mr: 1.5, fontSize: 28 }}
|
||||
/>
|
||||
|
||||
@@ -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.",
|
||||
"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",
|
||||
"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 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",
|
||||
"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",
|
||||
@@ -351,6 +353,7 @@
|
||||
"clash_mode_direct": "Direct Mode",
|
||||
"toggle_system_proxy": "Enable/Disable System Proxy",
|
||||
"toggle_tun_mode": "Enable/Disable Tun Mode",
|
||||
"entry_lightweight_mode": "Entry Lightweight Mode",
|
||||
"Backup Setting": "Backup Setting",
|
||||
"Backup Setting Info": "Support WebDAV backup configuration files",
|
||||
"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": "Enable Tray Icon",
|
||||
"LightWeight Mode": "Lightweight Mode",
|
||||
"LightWeight Mode Info": "Close the GUI and keep only the kernel running",
|
||||
"LightWeight Mode Settings": "LightWeight Mode Settings",
|
||||
@@ -570,7 +574,6 @@
|
||||
"No": "No",
|
||||
"Failed": "Failed",
|
||||
"Completed": "Completed",
|
||||
"Bahamut Anime": "Bahamut Anime",
|
||||
"Disallowed ISP": "Disallowed ISP",
|
||||
"Originals Only": "Originals Only",
|
||||
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
"Delete": "Удалить",
|
||||
"Enable": "Включить",
|
||||
"Disable": "Отключить",
|
||||
"Label-Home": "Главная",
|
||||
"Label-Proxies": "Прокси",
|
||||
"Label-Profiles": "Профили",
|
||||
"Label-Connections": "Соединения",
|
||||
"Label-Rules": "Правила",
|
||||
"Label-Logs": "Логи",
|
||||
"Label-Test": "Тест",
|
||||
"Label-Unlock": "Тест",
|
||||
"Label-Settings": "Настройки",
|
||||
"Proxies": "Прокси",
|
||||
"Proxy Groups": "Группы прокси",
|
||||
@@ -39,12 +40,12 @@
|
||||
"Sort by name": "Сортировать по названию",
|
||||
"Delay check URL": "URL проверки задержки",
|
||||
"Delay check to cancel fixed": "Проверка задержки для отмены фиксированного",
|
||||
"Proxy basic": "Резюме о прокси",
|
||||
"Proxy detail": "Подробности о прокси",
|
||||
"Proxy basic": "Отображать меньше сведений о прокси",
|
||||
"Proxy detail": "Отображать больше сведений о прокси",
|
||||
"Profiles": "Профили",
|
||||
"Update All Profiles": "Обновить все профили",
|
||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||
"Reactivate Profiles": "Реактивировать профили",
|
||||
"Reactivate Profiles": "Перезапустить профиль",
|
||||
"Paste": "Вставить",
|
||||
"Profile URL": "URL профиля",
|
||||
"Import": "Импорт",
|
||||
@@ -135,19 +136,19 @@
|
||||
"Hidden": "Скрытый",
|
||||
"Group Name Required": "Требуется имя группы",
|
||||
"Group Name Already Exists": "Имя группы уже существует",
|
||||
"Extend Config": "Изменить Merge.",
|
||||
"Extend Config": "Изменить Merge",
|
||||
"Extend Script": "Изменить Script",
|
||||
"Global Merge": "Глобальный расширенный Настройки",
|
||||
"Global Script": "Глобальный расширенный скрипт",
|
||||
"Type": "Тип",
|
||||
"Name": "Название",
|
||||
"Descriptions": "Описания",
|
||||
"Descriptions": "Описание",
|
||||
"Subscription URL": "URL подписки",
|
||||
"Update Interval": "Интервал обновления",
|
||||
"Choose File": "Выбрать файл",
|
||||
"Use System Proxy": "Использовать системный прокси для обновления",
|
||||
"Use Clash Proxy": "Использовать прокси Clash для обновления",
|
||||
"Accept Invalid Certs (Danger)": "Принимать недействительные сертификаты (Опасно)",
|
||||
"Accept Invalid Certs (Danger)": "Принимать недействительные сертификаты (ОПАСНО)",
|
||||
"Refresh": "Обновить",
|
||||
"Home": "Главная",
|
||||
"Select": "Выбрать",
|
||||
@@ -162,17 +163,19 @@
|
||||
"To Top": "Наверх",
|
||||
"To End": "Вниз",
|
||||
"Connections": "Соединения",
|
||||
"Table View": "Tablichnyy vid",
|
||||
"List View": "Spiskovyy vid",
|
||||
"Table View": "Отображать в виде таблицы",
|
||||
"List View": "Отображать в виде списка",
|
||||
"Close All": "Закрыть всё",
|
||||
"Default": "По умолчанию",
|
||||
"Download Speed": "Скорость загрузки",
|
||||
"Upload": "Загрузка",
|
||||
"Download": "Скачивание",
|
||||
"Download Speed": "Скорость скачивания",
|
||||
"Upload Speed": "Скорость загрузки",
|
||||
"Host": "Хост",
|
||||
"Downloaded": "Скачано",
|
||||
"Uploaded": "Загружено",
|
||||
"DL Speed": "Скорость загрузки",
|
||||
"UL Speed": "Скорость выгрузки",
|
||||
"DL Speed": "Скорость скачивания",
|
||||
"UL Speed": "Скорость загрузки",
|
||||
"Active Connections": "Активные соединения",
|
||||
"Chains": "Цепочки",
|
||||
"Rule": "Правило",
|
||||
"Process": "Процесс",
|
||||
@@ -182,31 +185,43 @@
|
||||
"DestinationPort": "Целевой порт",
|
||||
"Close Connection": "Закрыть соединение",
|
||||
"Rules": "Правила",
|
||||
"Rule Provider": "Провайдер правило",
|
||||
"Rule Provider": "Провайдеры правил",
|
||||
"Logs": "Логи",
|
||||
"Pause": "Пауза",
|
||||
"Clear": "Очистить",
|
||||
"Test": "Тест",
|
||||
"Test All": "Тест Все",
|
||||
"Test All": "Тестировать все",
|
||||
"Testing...": "Тестирование ...",
|
||||
"Create Test": "Создать тест",
|
||||
"Edit Test": "Редактировать тест",
|
||||
"Icon": "Икона",
|
||||
"Test URL": "Тестовый URL",
|
||||
"Icon": "Иконка",
|
||||
"Test URL": "URL проверка",
|
||||
"Settings": "Настройки",
|
||||
"System Setting": "Настройки системы",
|
||||
"Tun Mode": "Tun (виртуальный сетевой адаптер) режим",
|
||||
"Reset to Default": "Сбросить настройки по умолчанию",
|
||||
"Tun Mode Info": "Режим Tun (виртуальный сетевой адаптер): захватывает весь системный трафик, при включении нет необходимости включать системный прокси-сервер.",
|
||||
"Tun Mode": "Режим TUN",
|
||||
"TUN requires Service Mode": "Режим TUN требует установленную службу Clash Verge",
|
||||
"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": "Стек",
|
||||
"System and Mixed Can Only be Used in Service Mode": "Система и смешанные могут использоваться только в сервисном режиме",
|
||||
"System and Mixed Can Only be Used in Service Mode": "Стэк System и Mixed могут использоваться только в режиме системной службы",
|
||||
"Device": "Имя устройства",
|
||||
"Auto Route": "Автоматическая маршрутизация",
|
||||
"Strict Route": "Строгий маршрут",
|
||||
"Strict Route": "Строгая маршрутизация",
|
||||
"Auto Detect Interface": "Автоопределение интерфейса",
|
||||
"DNS Hijack": "DNS-перехват",
|
||||
"MTU": "Максимальная единица передачи",
|
||||
"Service Mode": "Режим сервиса",
|
||||
"Service Mode Info": "Установите сервисный режим перед включением режима TUN. Процесс ядра, запущенный службой, может получить разрешение на установку виртуальной сетевой карты (режим TUN).",
|
||||
"MTU": "MTU",
|
||||
"Service Mode": "Режим системной службы",
|
||||
"Service Mode Info": "Установите режим системной службы перед включением режима TUN. Процесс ядра, запущенный службой, может получить разрешение на установку виртуальной сетевой карты (режим TUN).",
|
||||
"Current State": "Текущее состояние",
|
||||
"pending": "Ожидающий",
|
||||
"installed": "Установленный",
|
||||
@@ -216,7 +231,7 @@
|
||||
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен",
|
||||
"Install": "Установить",
|
||||
"Uninstall": "Удалить",
|
||||
"Disable Service Mode": "Отключить режим обслуживания",
|
||||
"Disable Service Mode": "Отключить режим системной службы",
|
||||
"System Proxy": "Системный прокси",
|
||||
"System Proxy Info": "Разрешить изменение настроек прокси-сервера операционной системы. Если разрешение не удастся, измените настройки прокси-сервера операционной системы вручную",
|
||||
"System Proxy Setting": "Настройка системного прокси",
|
||||
@@ -226,30 +241,30 @@
|
||||
"Disabled": "Отключено",
|
||||
"Server Addr": "Адрес сервера: ",
|
||||
"Not available": "Недоступно",
|
||||
"Proxy Guard": "Защита прокси",
|
||||
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим программным обеспечением",
|
||||
"Proxy Guard": "Proxy Guard",
|
||||
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим ПО",
|
||||
"Guard Duration": "Период защиты",
|
||||
"Always use Default Bypass": "Всегда использовать стандартное обходное решение",
|
||||
"Use Bypass Check": "Используйте проверку обхода",
|
||||
"Proxy Bypass": "Игнорирование прокси: ",
|
||||
"Bypass": "Игнорирование: ",
|
||||
"Proxy Bypass": "Игнорируемые адреса: ",
|
||||
"Bypass": "Игнорируемые адреса: ",
|
||||
"Use PAC Mode": "Используйте режим PAC",
|
||||
"PAC Script Content": "Содержание сценария PAC",
|
||||
"PAC URL": "Адрес PAC: ",
|
||||
"Auto Launch": "Автозапуск",
|
||||
"Silent Start": "Тихий запуск",
|
||||
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
|
||||
"TG Channel": "Канал Telegram",
|
||||
"TG Channel": "Telegram-канал",
|
||||
"Manual": "Документация",
|
||||
"Github Repo": "GitHub репозиторий",
|
||||
"Clash Setting": "Настройки Clash",
|
||||
"Allow Lan": "Разрешить локальную сеть",
|
||||
"Allow Lan": "Разрешить доступ из локальной сети",
|
||||
"Network Interface": "Сетевой интерфейс",
|
||||
"Ip Address": "IP адрес",
|
||||
"Mac Address": "MAC адрес",
|
||||
"IPv6": "IPv6",
|
||||
"Unified Delay": "Общий задержка",
|
||||
"Unified Delay Info": "Когда унифицированная задержка включена, будут выполнены два теста задержки, чтобы устранить различия в задержке между разными типами узлов, вызванные подтверждением соединения и т. д",
|
||||
"Unified Delay": "Точная задержка",
|
||||
"Unified Delay Info": "Когда унифицированная(точная) задержка включена, будут выполнены два теста задержки, чтобы устранить различия в задержке между разными типами узлов, вызванные подтверждением соединения и т. д",
|
||||
"Log Level": "Уровень логов",
|
||||
"Log Level Info": "Это действует только на файлы журнала ядра в служебном файле в каталоге журналов.",
|
||||
"Port Config": "Настройка порта",
|
||||
@@ -259,32 +274,34 @@
|
||||
"Http Port": "Порт Http(s)-прокси",
|
||||
"Redir Port": "Порт прозрачного прокси Redir",
|
||||
"Tproxy Port": "Порт прозрачного прокси Tproxy",
|
||||
"External": "Внешний",
|
||||
"External": "Внешний контроллер",
|
||||
"External Controller": "Адрес прослушивания внешнего контроллера",
|
||||
"Core Secret": "Секрет",
|
||||
"Recommended": "Рекомендуется",
|
||||
"Open URL": "Открыть URL",
|
||||
"Replace host, port, secret with %host, %port, %secret": "Замените хост, порт, секрет на %host, %port, %secret",
|
||||
"Support %host, %port, %secret": "Поддержка %host, %port, %secret",
|
||||
"Clash Core": "Ядра Clash",
|
||||
"Upgrade": "Обновлять",
|
||||
"Restart": "Перезапуск",
|
||||
"Open URL": "Перейти по адресу",
|
||||
"Replace host, port, secret with %host, %port, %secret": "Замените хост, порт и секрет на %host, %port, %secret",
|
||||
"Support %host, %port, %secret": "Поддерживаются %host, %port, %secret",
|
||||
"Clash Core": "Ядро Clash",
|
||||
"Upgrade": "Обновить",
|
||||
"Restart": "Перезапустить",
|
||||
"Release Version": "Официальная версия",
|
||||
"Alpha Version": "Альфа-версия",
|
||||
"Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания",
|
||||
"Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим системной службы",
|
||||
"Please enter your root password": "Пожалуйста, введите ваш пароль root",
|
||||
"Grant": "Предоставить",
|
||||
"Open UWP tool": "Открыть UWP инструмент",
|
||||
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
|
||||
"Update GeoData": "Обновление GeoData",
|
||||
"Verge Setting": "Настройки Verge",
|
||||
"Update GeoData": "Обновить GeoData",
|
||||
"Verge Basic Setting": "Основные настройки Verge",
|
||||
"Verge Advanced Setting": "Расширенные настройки Verge",
|
||||
"Language": "Язык",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Theme Mode": "Цветовая тема",
|
||||
"theme.light": "Светлая",
|
||||
"theme.dark": "Тёмная",
|
||||
"theme.system": "Системная",
|
||||
"Tray Click Event": "Событие щелчка в лотке",
|
||||
"Tray Click Event": "Событие при щелчке по иконке в трее",
|
||||
"Show Main Window": "Показать главное окно",
|
||||
"Show Tray Menu": "Показать меню в трее",
|
||||
"Copy Env Type": "Скопировать тип Env",
|
||||
"Copy Success": "Скопировано",
|
||||
"Start Page": "Главная страница",
|
||||
@@ -293,8 +310,8 @@
|
||||
"Theme Setting": "Настройки темы",
|
||||
"Primary Color": "Основной цвет",
|
||||
"Secondary Color": "Вторичный цвет",
|
||||
"Primary Text Color": "Основной текст",
|
||||
"Secondary Text Color": "Вторичный текст",
|
||||
"Primary Text": "Первичный текст",
|
||||
"Secondary Text": "Вторичный текст",
|
||||
"Info Color": "Информационный цвет",
|
||||
"Warning Color": "Цвет предупреждения",
|
||||
"Error Color": "Цвет ошибки",
|
||||
@@ -306,50 +323,53 @@
|
||||
"Memory Usage": "Использование памяти",
|
||||
"Memory Cleanup": "Нажмите, чтобы очистить память",
|
||||
"Proxy Group Icon": "Иконка Группы прокси",
|
||||
"Nav Icon": "Иконка навигации",
|
||||
"Monochrome": "Монохромный",
|
||||
"Colorful": "Полноцветный",
|
||||
"Tray Icon": "Иконка лотка",
|
||||
"Common Tray Icon": "Общий значок в лотке",
|
||||
"System Proxy Tray Icon": "Значок системного прокси в лотке",
|
||||
"Tun Tray Icon": "Значок туннеля в лотке",
|
||||
"Miscellaneous": "Настройки Прочие",
|
||||
"Nav Icon": "Иконки навигации",
|
||||
"Monochrome": "Монохромные",
|
||||
"Colorful": "Цветные",
|
||||
"Tray Icon": "Иконка в трее",
|
||||
"Common Tray Icon": "Общий значок в трее",
|
||||
"System Proxy Tray Icon": "Значок системного прокси в трее",
|
||||
"Tun Tray Icon": "Значок TUN в трее",
|
||||
"Miscellaneous": "Расширенные настройки",
|
||||
"App Log Level": "Уровень журнала приложения",
|
||||
"Auto Close Connections": "Автоматическое закрытие соединений",
|
||||
"Auto Close Connections Info": "Завершить установленные соединения при изменении выбора группы прокси или режима прокси",
|
||||
"Auto Close Connections Info": "Закрыть установленные соединения при изменении выбора группы прокси или режима прокси",
|
||||
"Auto Check Update": "Автоматическая проверка обновлений",
|
||||
"Enable Builtin Enhanced": "Включить встроенные улучшения",
|
||||
"Enable Builtin Enhanced Info": "Обработка совместимости для файла конфигурации",
|
||||
"Proxy Layout Columns": "Количество столбцов в макете прокси",
|
||||
"Auto Columns": "Авто колонки",
|
||||
"Auto Log Clean": "Автоматическая очистка журналов",
|
||||
"Auto Log Clean": "Автоматическая очистка логов",
|
||||
"Never Clean": "Никогда не очищать",
|
||||
"Retain _n Days": "Сохранять {{n}} дней",
|
||||
"Default Latency Test": "Ссылка на тестирование задержки по умолчанию",
|
||||
"Default Latency Test": "Ссылка на тест задержки",
|
||||
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
|
||||
"Default Latency Timeout": "Таймаут задержки по умолчанию",
|
||||
"Hotkey Setting": "Настройки клавиатурных сокращений",
|
||||
"Hotkey Setting": "Настройки сочетаний клавиш",
|
||||
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
|
||||
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
"clash_mode_global": "Глобальный режим",
|
||||
"clash_mode_direct": "Прямой режим",
|
||||
"toggle_system_proxy": "Включить/Отключить системный прокси",
|
||||
"toggle_tun_mode": "Включить/Отключить режим туннеля",
|
||||
"toggle_tun_mode": "Включить/Отключить режим TUN",
|
||||
"entry_lightweight_mode": "Вход в LightWeight Mode",
|
||||
"Backup Setting": "Настройки резервного копирования",
|
||||
"Backup Setting Info": "Поддерживает файлы конфигурации резервного копирования WebDAV",
|
||||
"Runtime Config": "Используемый конфиг",
|
||||
"Open Conf Dir": "Открыть папку приложения",
|
||||
"Open Conf Dir Info": "Если программное обеспечение работает ненормально, сделайте резервную копию и удалите все файлы в этой папке, а затем перезапустите программное обеспечение",
|
||||
"Open Conf Dir Info": "Если программное обеспечение работает неправильно, сделайте резервную копию и удалите все файлы в этой папке, а затем перезапустите ПО",
|
||||
"Open Core Dir": "Открыть папку ядра",
|
||||
"Open Logs Dir": "Открыть папку логов",
|
||||
"Check for Updates": "Проверить обновления",
|
||||
"Go to Release Page": "Перейти на страницу релизов",
|
||||
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную",
|
||||
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените файлы вручную",
|
||||
"Break Change Update Error": "Это крупное обновление, которое не поддерживает обновление внутри приложения. Пожалуйста, удалите его и загрузите установочный файл вручную.",
|
||||
"Open Dev Tools": "Открыть инструменты разработчика",
|
||||
"Open Dev Tools": "Открыть Dev Tools",
|
||||
"Export Diagnostic Info": "Экспорт диагностической информации",
|
||||
"Export Diagnostic Info For Issue Reporting": "Экспорт диагностической информации для отчета об ошибке",
|
||||
"Exit": "Выход",
|
||||
"Verge Version": "Версия Verge",
|
||||
"Verge Version": "Версия Clash Verge Rev",
|
||||
"ReadOnly": "Только для чтения",
|
||||
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
|
||||
"Filter": "Фильтр",
|
||||
@@ -359,23 +379,24 @@
|
||||
"Use Regular Expression": "Использовать регулярные выражения",
|
||||
"Profile Imported Successfully": "Профиль успешно импортирован",
|
||||
"Profile Switched": "Профиль изменен",
|
||||
"Profile Reactivated": "Профиль повторно активирован",
|
||||
"Profile Reactivated": "Профиль перезапущен",
|
||||
"Only YAML Files Supported": "Поддерживаются только файлы YAML",
|
||||
"Settings Applied": "Применены настройки",
|
||||
"Settings Applied": "Настройки применены",
|
||||
"Installing Service...": "Установка службы...",
|
||||
"Service Installed Successfully": "Служба успешно установлена",
|
||||
"Service Uninstalled Successfully": "Служба успешно удалена",
|
||||
"Proxy Daemon Duration Cannot be Less than 1 Second": "Продолжительность работы прокси-демона не может быть меньше 1 секунды",
|
||||
"Invalid Bypass Format": "Неверный формат обхода",
|
||||
"Clash Port Modified": "Clash порт изменен",
|
||||
"Clash Port Modified": "Порт Clash изменен",
|
||||
"Port Conflict": "Конфликт портов",
|
||||
"Restart Application to Apply Modifications": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
|
||||
"External Controller Address Modified": "Изменен адрес внешнего контроллера",
|
||||
"External Controller Address Modified": "Настройки внешнего контроллера изменены",
|
||||
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
|
||||
"Core Version Updated": "Обновлена версия ядра",
|
||||
"Clash Core Restarted": "Clash ядра перезапущено",
|
||||
"GeoData Updated": "GeoData Обновлена",
|
||||
"Currently on the Latest Version": "В настоящее время используется последняя версия",
|
||||
"Import subscription successful": "Импорт подписки успешно",
|
||||
"Core Version Updated": "Ядро обновлено до последней версии",
|
||||
"Clash Core Restarted": "Ядро перезапущено",
|
||||
"GeoData Updated": "Файлы GeoData обновлены",
|
||||
"Currently on the Latest Version": "Обновление не требуется",
|
||||
"Import Subscription Successful": "Подписка успешно импортирована",
|
||||
"WebDAV Server URL": "URL-адрес сервера WebDAV http(s)://",
|
||||
"Username": "Имя пользователя",
|
||||
"Password": "Пароль",
|
||||
@@ -410,12 +431,12 @@
|
||||
"PAC File": "PAC файл",
|
||||
"Web UI": "Веб-интерфейс",
|
||||
"Hotkeys": "Горячие клавиши",
|
||||
"Verge Mixed Port": "Смешанный порт Verge",
|
||||
"Verge Socks Port": "Порт Verge Socks",
|
||||
"Verge Redir Port": "Порт перенаправления Verge",
|
||||
"Verge Tproxy Port": "Порт Verge Tproxy",
|
||||
"Verge Mixed Port": "Mixed порт",
|
||||
"Verge Socks Port": "Порт Socks",
|
||||
"Verge Redir Port": "Порт Redir",
|
||||
"Verge Tproxy Port": "Порт Tproxy",
|
||||
"Verge Port": "Порт Verge",
|
||||
"Verge HTTP Enabled": "HTTP Verge включен",
|
||||
"Verge HTTP Enabled": "HTTP включен",
|
||||
"WebDAV URL": "URL WebDAV",
|
||||
"WebDAV Username": "Имя пользователя WebDAV",
|
||||
"WebDAV Password": "Пароль WebDAV",
|
||||
@@ -432,9 +453,16 @@
|
||||
"Rule Mode": "Режим правил",
|
||||
"Global Mode": "Глобальный режим",
|
||||
"Direct Mode": "Прямой режим",
|
||||
"Enable Tray Speed": "Включить скорость в лотке",
|
||||
"LightWeight Mode": "Облегченный режим",
|
||||
"LightWeight Mode Info": "Закройте графический интерфейс и оставьте работать только ядро",
|
||||
"Enable Tray Speed": "Показывать скорость в трее",
|
||||
"Enable Tray Icon": "Показывать значок в трее",
|
||||
"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": "Ошибка проверки конфигурации подписки, проверьте файл конфигурации, изменения отменены, ошибка:",
|
||||
"Boot Config Validation Failed": "Ошибка проверки конфигурации при запуске, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
||||
"Core Change Config Validation Failed": "Ошибка проверки конфигурации при смене ядра, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
|
||||
@@ -443,12 +471,112 @@
|
||||
"Script Missing Main": "Ошибка скрипта, изменения отменены",
|
||||
"File Not Found": "Файл не найден, изменения отменены",
|
||||
"Script File Error": "Ошибка файла скрипта, изменения отменены",
|
||||
"Core Changed Successfully": "Ядро успешно сменено",
|
||||
"Core Changed Successfully": "Ядро успешно изменено",
|
||||
"Failed to Change Core": "Не удалось сменить ядро",
|
||||
"Verge Basic Setting": "Основные настройки Verge",
|
||||
"Verge Advanced Setting": "Расширенные настройки Verge",
|
||||
"TUN requires Service Mode": "Режим TUN требует обслуживания",
|
||||
"Install Service": "Установить службу",
|
||||
"Installing Service...": "Установка службы...",
|
||||
"Service Administrator Prompt": "Clash Verge требует прав администратора для переустановки системной службы"
|
||||
"YAML Syntax Error": "Ошибка синтаксиса YAML, откат изменений",
|
||||
"YAML Read Error": "Ошибка чтения YAML, откат изменений",
|
||||
"YAML Mapping Error": "Ошибка YAML Mapping, откат изменений",
|
||||
"YAML Key Error": "Ошибка ключа YAML, откат изменений",
|
||||
"YAML Error": "Ошибка YAML, откат изменений",
|
||||
"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)": "Ошибка подключения"
|
||||
}
|
||||
|
||||
@@ -205,8 +205,10 @@
|
||||
"Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
|
||||
"System Proxy Enabled": "系统代理已启用,您的应用将通过代理访问网络",
|
||||
"System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项",
|
||||
"TUN Mode Enabled": "TUN 模式已启用,应用将通过虚拟网卡访问网络",
|
||||
"TUN Mode Disabled": "TUN 模式已关闭,适用于特殊应用",
|
||||
"TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务",
|
||||
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊应用",
|
||||
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊不遵循系统代理设置的应用",
|
||||
"rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
||||
"global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
||||
"direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
||||
@@ -351,6 +353,8 @@
|
||||
"clash_mode_direct": "直连模式",
|
||||
"toggle_system_proxy": "打开/关闭系统代理",
|
||||
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
||||
"toggle_lightweight_mode": "进入轻量模式",
|
||||
"entry_lightweight_mode": "进入轻量模式",
|
||||
"Backup Setting": "备份设置",
|
||||
"Backup Setting Info": "支持 WebDAV 备份配置文件",
|
||||
"Runtime Config": "当前配置",
|
||||
@@ -451,6 +455,7 @@
|
||||
"Global Mode": "全局模式",
|
||||
"Direct Mode": "直连模式",
|
||||
"Enable Tray Speed": "启用托盘速率",
|
||||
"Enable Tray Icon": "启用托盘图标",
|
||||
"LightWeight Mode": "轻量模式",
|
||||
"LightWeight Mode Info": "关闭GUI界面,仅保留内核运行",
|
||||
"LightWeight Mode Settings": "轻量模式设置",
|
||||
@@ -570,7 +575,6 @@
|
||||
"No": "不支持",
|
||||
"Failed": "测试失败",
|
||||
"Completed": "检测完成",
|
||||
"Bahamut Anime": "动画疯",
|
||||
"Disallowed ISP": "不允许的 ISP",
|
||||
"Originals Only": "仅限原创",
|
||||
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
|
||||
|
||||
@@ -37,7 +37,11 @@ import { BasePage } from "@/components/base";
|
||||
import { ClashInfoCard } from "@/components/home/clash-info-card";
|
||||
import { SystemInfoCard } from "@/components/home/system-info-card";
|
||||
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 { IpInfoCard } from "@/components/home/ip-info-card";
|
||||
|
||||
@@ -260,7 +264,11 @@ const HomePage = () => {
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<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 />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -16,6 +16,12 @@ const ProxyPage = () => {
|
||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
dedupingInterval: 1000,
|
||||
errorRetryInterval: 5000
|
||||
}
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
|
||||
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@@ -738,6 +738,7 @@ interface IVergeConfig {
|
||||
sysproxy_tray_icon?: boolean;
|
||||
tun_tray_icon?: boolean;
|
||||
enable_tray_speed?: boolean;
|
||||
enable_tray_icon?: boolean;
|
||||
enable_tun_mode?: boolean;
|
||||
enable_auto_light_weight_mode?: boolean;
|
||||
auto_light_weight_minutes?: number;
|
||||
|
||||
Reference in New Issue
Block a user