Compare commits

...

48 Commits

48 changed files with 1963 additions and 667 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 }} \

View File

@@ -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

View File

@@ -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 -"
},

View 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);

View File

@@ -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();
}

View File

@@ -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
View File

@@ -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"

View File

@@ -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"] }

View 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)
}

View File

@@ -6,4 +6,4 @@ use super::CmdResult;
pub async fn entry_lightweight_mode() -> CmdResult {
lightweight::entry_lightweight_mode();
Ok(())
}
}

View File

@@ -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::*;

View File

@@ -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()

View File

@@ -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,
}
}
}

View File

@@ -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);
}
}
_ => {}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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(),

View File

@@ -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)
}
}
}

View File

@@ -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));

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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")]

View File

@@ -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(())
}

View File

@@ -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!({

View File

@@ -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"
}

View File

@@ -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>
))}

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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={{

View File

@@ -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>
);

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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")} />

View File

@@ -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>
);
});
});

View File

@@ -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 }}
/>

View File

@@ -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+)",

View File

@@ -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)": "Ошибка подключения"
}

View File

@@ -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+禁止)",

View File

@@ -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>

View File

@@ -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();

View File

@@ -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;