Compare commits

..
69 changed files with 2643 additions and 2433 deletions
+26 -24
View File
@@ -253,12 +253,10 @@ jobs:
fail-fast: false
matrix:
include:
# It should be ubuntu-22.04 to match the cross-compilation environment
# ortherwise it is hard to resolve the dependencies
- os: ubuntu-22.04
- os: ubuntu-24.04
target: aarch64-unknown-linux-gnu
arch: arm64
- os: ubuntu-22.04
- os: ubuntu-24.04
target: armv7-unknown-linux-gnueabihf
arch: armhf
runs-on: ${{ matrix.os }}
@@ -313,35 +311,39 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
- name: Setup for linux
run: |
sudo dpkg --add-architecture ${{ matrix.arch }}
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
sudo rm -f /etc/apt/sources.list.d/ubuntu.sources
sudo tee /etc/apt/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-security main restricted universe multiverse
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu noble-backports main restricted universe multiverse
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
deb [arch=${{ matrix.arch }}] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse
EOF
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
sudo mv /tmp/sources.list /etc/apt/sources.list
sudo apt-get update -y
sudo dpkg --add-architecture ${{ matrix.arch }}
sudo apt update
sudo apt-get install -y libglib2.0-dev-bin
sudo apt install -y \
libxslt1.1:${{ matrix.arch }} \
sudo apt-get install -y \
linux-libc-dev:${{ matrix.arch }} \
libc6-dev:${{ matrix.arch }} \
libicu-dev:${{ matrix.arch }}
sudo apt-get install -y --no-install-recommends \
libxslt1-dev:${{ matrix.arch }} \
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
libayatana-appindicator3-dev:${{ matrix.arch }} \
libssl-dev:${{ matrix.arch }} \
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
librsvg2-dev:${{ matrix.arch }} \
patchelf
- name: Install aarch64 tools
if: matrix.target == 'aarch64-unknown-linux-gnu'
-5
View File
@@ -1,5 +0,0 @@
queue_rules:
- name: LetMeMergeForYou
batch_size: 3
allow_queue_branch_edit: true
queue_conditions: []
Generated
+355 -262
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -5,6 +5,7 @@ members = [
"crates/clash-verge-logging",
"crates/clash-verge-signal",
"crates/tauri-plugin-clash-verge-sysinfo",
"crates/clash-verge-types",
"crates/clash-verge-i18n",
]
resolver = "2"
@@ -43,6 +44,7 @@ strip = false
clash-verge-draft = { path = "crates/clash-verge-draft" }
clash-verge-logging = { path = "crates/clash-verge-logging" }
clash-verge-signal = { path = "crates/clash-verge-signal" }
clash-verge-types = { path = "crates/clash-verge-types" }
clash-verge-i18n = { path = "crates/clash-verge-i18n" }
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
@@ -57,7 +59,7 @@ tokio = { version = "1.49.0", features = [
"time",
"sync",
] }
flexi_logger = "0.31.8"
flexi_logger = "0.31.7"
log = "0.4.29"
smartstring = { version = "1.0.1" }
+1 -5
View File
@@ -1,6 +1,6 @@
## v2.4.5
- **Mihomo(Meta) 内核升级至 v1.19.19**
- **Mihomo(Meta) 内核升级至 v1.19.18**
### 🐞 修复问题
@@ -12,9 +12,6 @@
- 修复 Windows 下系统主题同步问题
- 修复 URL Schemes 无法正常导入
- 修复 Linux 下无法安装 TUN 服务
- 修复可能的端口被占用误报
- 修复设置允许外部控制来源不能立即生效
- 修复前端性能回归问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
@@ -48,6 +45,5 @@
- 保存 TUN 设置时优化执行流程,避免界面卡顿
- 补充 `deb` / `rpm` 依赖 `libayatana-appindicator`
- 「连接」表格标题的排序点击区域扩展到整列宽度
- 备份恢复时显示加载覆盖层,恢复过程无需再手动关闭对话框
</details>
+13
View File
@@ -66,6 +66,19 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
🌐 官网:👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
🧩 YXVM 独立服务器优势:
- 🌎 优质网络,回程优化,下载快到飞起
- 🔧 物理机独享资源,非VPS可比,性能拉满
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
- 📦 本项目使用配置已在售,欢迎同款入手!
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
## Features
- 基于性能强劲的 Rust 和 Tauri 2 框架
+46 -46
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: لوحة التحكم
body: تم تحديث حالة عرض لوحة التحكم.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: تبديل الوضع
body: تم التبديل إلى {mode}.
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: وكيل النظام
body: تم تحديث حالة وكيل النظام.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: وضع TUN
body: تم تحديث حالة وضع TUN.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: الوضع الخفيف
body: تم الدخول إلى الوضع الخفيف.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: الملفات التعريفية
body: تمت إعادة تفعيل الملف التعريفي.
title: Profiles
body: Profile Reactivated.
appQuit:
title: على وشك الخروج
body: Clash Verge على وشك الخروج.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: تم إخفاء التطبيق
body: Clash Verge يعمل في الخلفية.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: لوحة التحكم
ruleMode: وضع القواعد
globalMode: الوضع العام
directMode: الوضع المباشر
outboundModes: أوضاع الخروج
rule: قاعدة
direct: مباشر
global: عام
profiles: الملفات التعريفية
proxies: وكلاء
systemProxy: وكيل النظام
tunMode: وضع TUN
closeAllConnections: إغلاق كل الاتصالات
lightweightMode: الوضع الخفيف
copyEnv: نسخ متغيرات البيئة
confDir: دليل الإعدادات
coreDir: دليل النواة
logsDir: دليل السجلات
openDir: فتح الدليل
appLog: سجل التطبيق
coreLog: سجل النواة
restartClash: إعادة تشغيل نواة Clash
restartApp: إعادة تشغيل التطبيق
vergeVersion: إصدار Verge
more: المزيد
exit: خروج
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: وكيل النظام
systemProxy: System Proxy
tun: TUN
profile: ملف تعريفي
profile: Profile
+43 -43
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Übersicht
body: Die Sichtbarkeit der Übersicht wurde aktualisiert.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Moduswechsel
body: Auf {mode} umgeschaltet.
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: Systemproxy
body: Der Status des Systemproxys wurde aktualisiert.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN-Modus
body: Der Status des TUN-Modus wurde aktualisiert.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Leichtmodus
body: Leichtmodus aktiviert.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profile
body: Profil reaktiviert.
title: Profiles
body: Profile Reactivated.
appQuit:
title: Beenden steht bevor
body: Clash Verge wird gleich beendet.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Anwendung ausgeblendet
body: Clash Verge läuft im Hintergrund.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Übersicht
ruleMode: Regelmodus
globalMode: Globaler Modus
directMode: Direktmodus
outboundModes: Ausgangsmodi
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Regel
direct: Direkt
global: Global
profiles: Profile
proxies: Proxy
systemProxy: Systemproxy
tunMode: TUN-Modus
closeAllConnections: Alle Verbindungen schließen
lightweightMode: Leichtmodus
copyEnv: Umgebungsvariablen kopieren
confDir: Konfigurationsverzeichnis
coreDir: Core-Verzeichnis
logsDir: Log-Verzeichnis
openDir: Verzeichnis öffnen
appLog: Anwendungslog
coreLog: Core-Log
restartClash: Clash-Core neu starten
restartApp: Anwendung neu starten
vergeVersion: Verge-Version
more: Mehr
exit: Beenden
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: Systemproxy
systemProxy: System Proxy
tun: TUN
profile: Profil
profile: Profile
+42 -42
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Panel
body: La visibilidad del panel se ha actualizado.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Cambio de modo
body: Cambiado a {mode}.
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: Proxy del sistema
body: El estado del proxy del sistema se ha actualizado.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: Modo TUN
body: El estado del modo TUN se ha actualizado.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Modo ligero
body: Se ha entrado en el modo ligero.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Perfiles
body: Perfil reactivado.
title: Profiles
body: Profile Reactivated.
appQuit:
title: A punto de salir
body: Clash Verge está a punto de salir.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Aplicación oculta
body: Clash Verge se está ejecutando en segundo plano.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Panel
ruleMode: Modo de reglas
globalMode: Modo global
directMode: Modo directo
outboundModes: Modos de salida
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Regla
direct: Directo
global: Global
profiles: Perfiles
profiles: Profiles
proxies: Proxies
systemProxy: Proxy del sistema
tunMode: Modo TUN
closeAllConnections: Cerrar todas las conexiones
lightweightMode: Modo ligero
copyEnv: Copiar variables de entorno
confDir: Directorio de configuración
coreDir: Directorio del núcleo
logsDir: Directorio de registros
openDir: Abrir directorio
appLog: Registro de la aplicación
coreLog: Registro del núcleo
restartClash: Reiniciar el núcleo de Clash
restartApp: Reiniciar aplicación
vergeVersion: Versión de Verge
more: Más
exit: Salir
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: Proxy del sistema
systemProxy: System Proxy
tun: TUN
profile: Perfil
profile: Profile
+46 -46
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: داشبورد
body: وضعیت نمایش داشبورد به‌روزرسانی شد.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: تغییر حالت
body: به {mode} تغییر کرد.
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: پروکسی سیستم
body: وضعیت پروکسی سیستم به‌روزرسانی شد.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: حالت TUN
body: وضعیت حالت TUN به‌روزرسانی شد.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: حالت سبک
body: به حالت سبک وارد شد.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: پروفایل‌ها
body: پروفایل دوباره فعال شد.
title: Profiles
body: Profile Reactivated.
appQuit:
title: در آستانه خروج
body: Clash Verge در آستانه خروج است.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: برنامه پنهان شد
body: Clash Verge در پس‌زمینه در حال اجراست.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: داشبورد
ruleMode: حالت قوانین
globalMode: حالت سراسری
directMode: حالت مستقیم
outboundModes: حالت‌های خروجی
rule: قانون
direct: مستقیم
global: سراسری
profiles: پروفایل‌ها
proxies: پروکسی‌ها
systemProxy: پروکسی سیستم
tunMode: حالت TUN
closeAllConnections: بستن همه اتصال‌ها
lightweightMode: حالت سبک
copyEnv: کپی متغیرهای محیطی
confDir: پوشه پیکربندی
coreDir: پوشه هسته
logsDir: پوشه گزارش‌ها
openDir: باز کردن پوشه
appLog: گزارش برنامه
coreLog: گزارش هسته
restartClash: راه‌اندازی مجدد هسته Clash
restartApp: راه‌اندازی مجدد برنامه
vergeVersion: نسخه Verge
more: بیشتر
exit: خروج
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: پروکسی سیستم
systemProxy: System Proxy
tun: TUN
profile: پروفایل
profile: Profile
+43 -43
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Dasbor
body: Visibilitas dasbor telah diperbarui.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Peralihan Mode
body: Beralih ke {mode}.
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: Proksi Sistem
body: Status proksi sistem telah diperbarui.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: Mode TUN
body: Status mode TUN telah diperbarui.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Mode Ringan
body: Masuk ke mode ringan.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profil
body: Profil diaktifkan kembali.
title: Profiles
body: Profile Reactivated.
appQuit:
title: Akan Keluar
body: Clash Verge akan keluar.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Aplikasi Disembunyikan
body: Clash Verge berjalan di latar belakang.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Dasbor
ruleMode: Mode Aturan
globalMode: Mode Global
directMode: Mode Langsung
outboundModes: Mode Keluar
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Aturan
direct: Langsung
global: Global
profiles: Profil
proxies: Proksi
systemProxy: Proksi Sistem
tunMode: Mode TUN
closeAllConnections: Tutup Semua Koneksi
lightweightMode: Mode Ringan
copyEnv: Salin Variabel Lingkungan
confDir: Direktori Konfigurasi
coreDir: Direktori Core
logsDir: Direktori Log
openDir: Buka Direktori
appLog: Log Aplikasi
coreLog: Log Core
restartClash: Mulai Ulang Core Clash
restartApp: Mulai Ulang Aplikasi
vergeVersion: Versi Verge
more: Lainnya
exit: Keluar
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: Proksi Sistem
systemProxy: System Proxy
tun: TUN
profile: Profil
profile: Profile
+43 -43
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: ダッシュボード
body: ダッシュボードの表示状態が更新されました。
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: モード切り替え
body: "{mode} に切り替えました。"
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: システムプロキシ
body: システムプロキシの状態が更新されました。
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN モード
body: TUN モードの状態が更新されました。
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: 軽量モード
body: 軽量モードに入りました。
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: プロファイル
body: プロファイルが再有効化されました。
title: Profiles
body: Profile Reactivated.
appQuit:
title: 終了間近
body: Clash Verge はまもなく終了します。
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: アプリが非表示
body: Clash Verge はバックグラウンドで実行中です。
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: ダッシュボード
ruleMode: ルールモード
globalMode: グローバルモード
directMode: ダイレクトモード
outboundModes: アウトバウンドモード
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: ルール
direct: ダイレクト
global: グローバル
profiles: プロファイル
proxies: プロキシ
systemProxy: システムプロキシ
tunMode: TUN モード
closeAllConnections: すべての接続を閉じる
lightweightMode: 軽量モード
copyEnv: 環境変数をコピー
confDir: 設定ディレクトリ
coreDir: コアディレクトリ
logsDir: ログディレクトリ
openDir: ディレクトリを開く
appLog: アプリケーションログ
coreLog: コアログ
restartClash: Clash コアを再起動
restartApp: アプリケーションを再起動
vergeVersion: Verge バージョン
more: その他
exit: 終了
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: システムプロキシ
systemProxy: System Proxy
tun: TUN
profile: プロファイル
profile: Profile
+5 -5
View File
@@ -16,8 +16,8 @@ notifications:
title: 경량 모드
body: 경량 모드에 진입했습니다.
profilesReactivated:
title: 프로필
body: 프로필이 다시 활성화되었습니다.
title: Profiles
body: Profile Reactivated.
appQuit:
title: 곧 종료
body: Clash Verge가 곧 종료됩니다.
@@ -25,14 +25,14 @@ notifications:
title: 앱이 숨겨짐
body: Clash Verge가 백그라운드에서 실행 중입니다.
service:
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: 대시보드
ruleMode: 규칙 모드
globalMode: 전역 모드
directMode: 직접 모드
outboundModes: 아웃바운드 모드
outboundModes: Outbound Modes
rule: 규칙
direct: 직접
global: 글로벌
+43 -43
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Панель
body: Видимость панели обновлена.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Смена режима
body: Переключено на {mode}.
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: Системный прокси
body: Статус системного прокси обновлен.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: Режим TUN
body: Статус режима TUN обновлен.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Легкий режим
body: Включен легкий режим.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Профили
body: Профиль повторно активирован.
title: Profiles
body: Profile Reactivated.
appQuit:
title: Скорый выход
body: Clash Verge скоро завершит работу.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Приложение скрыто
body: Clash Verge работает в фоновом режиме.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Панель
ruleMode: Режим правил
globalMode: Глобальный режим
directMode: Прямой режим
outboundModes: Исходящие режимы
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Правило
direct: Прямой
global: Глобальный
profiles: Профили
proxies: Прокси
systemProxy: Системный прокси
tunMode: Режим TUN
closeAllConnections: Закрыть все соединения
lightweightMode: Легкий режим
copyEnv: Копировать переменные среды
confDir: Каталог конфигурации
coreDir: Каталог ядра
logsDir: Каталог журналов
openDir: Открыть каталог
appLog: Журнал приложения
coreLog: Журнал ядра
restartClash: Перезапустить ядро Clash
restartApp: Перезапустить приложение
vergeVersion: Версия Verge
more: Еще
exit: Выход
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: Системный прокси
systemProxy: System Proxy
tun: TUN
profile: Профиль
profile: Profile
+43 -43
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Gösterge Paneli
body: Gösterge panelinin görünürlüğü güncellendi.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mod Değişimi
body: "{mode} moduna geçildi."
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: Sistem Vekil'i
body: Sistem vekil'i durumu güncellendi.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Modu
body: TUN modu durumu güncellendi.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Hafif Mod
body: Hafif moda geçildi.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Profiller
body: Profil yeniden etkinleştirildi.
title: Profiles
body: Profile Reactivated.
appQuit:
title: Çıkış Yapılmak Üzere
body: Clash Verge kapanmak üzere.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Uygulama Gizlendi
body: Clash Verge arka planda çalışıyor.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Gösterge Paneli
ruleMode: Kural Modu
globalMode: Küresel Mod
directMode: Doğrudan Mod
outboundModes: Giden Modlar
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Kural
direct: Doğrudan
global: Küresel
profiles: Profiller
proxies: Vekil'ler
systemProxy: Sistem Vekil'i
tunMode: TUN Modu
closeAllConnections: Tüm Bağlantıları Kapat
lightweightMode: Hafif Mod
copyEnv: Ortam Değişkenlerini Kopyala
confDir: Yapılandırma Dizini
coreDir: Çekirdek Dizini
logsDir: Günlük Dizini
openDir: Dizini Aç
appLog: Uygulama Günlüğü
coreLog: Çekirdek Günlüğü
restartClash: Clash Çekirdeğini Yeniden Başlat
restartApp: Uygulamayı Yeniden Başlat
vergeVersion: Verge Sürümü
more: Daha Fazla
exit: Çıkış
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: Sistem Vekil'i
systemProxy: System Proxy
tun: TUN
profile: Profil
profile: Profile
+46 -46
View File
@@ -1,60 +1,60 @@
_version: 1
notifications:
dashboardToggled:
title: Идарә панеле
body: Идарә панеленең күренеше яңартылды.
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Режим алыштыру
body: "{mode} режимына күчтел."
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: Системалы прокси
body: Системалы прокси хәле яңартылды.
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN режимы
body: TUN режимы хәле яңартылды.
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Җиңел режим
body: Җиңел режимга күчелде.
title: Lightweight Mode
body: Entered lightweight mode.
profilesReactivated:
title: Профильләр
body: Профиль яңадан активлаштырылды.
title: Profiles
body: Profile Reactivated.
appQuit:
title: Чыгар алдыннан
body: Clash Verge чыгарга җыена.
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Кушымта яшерелде
body: Clash Verge фон режимында эшли.
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
tray:
dashboard: Идарә панеле
ruleMode: Кагыйдә режимы
globalMode: Глобаль режим
directMode: Турыдан-туры режим
outboundModes: Чыгыш режимнары
rule: Кагыйдә
direct: Турыдан-туры
global: Глобаль
profiles: Профильләр
proxies: Проксилар
systemProxy: Системалы прокси
tunMode: TUN режимы
closeAllConnections: Барлык тоташуларны ябу
lightweightMode: Җиңел режим
copyEnv: Мохит үзгәрүчәннәрен күчерү
confDir: Конфигурация каталогы
coreDir: Ядро каталогы
logsDir: Журнал каталогы
openDir: Каталогны ачу
appLog: Кушымта журналы
coreLog: Ядро журналы
restartClash: Clash ядрәсен кабат җибәрү
restartApp: Кушымтаны кабат җибәрү
vergeVersion: Verge версиясе
more: Күбрәк
exit: Чыгу
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
outboundModes: Outbound Modes
rule: Rule
direct: Direct
global: Global
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: Системалы прокси
systemProxy: System Proxy
tun: TUN
profile: Профиль
profile: Profile
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "clash-verge-types"
version = "0.1.0"
edition = "2024"
rust-version = "1.91"
[dependencies]
serde = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true }
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
pub mod runtime;
@@ -2,8 +2,6 @@ use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use crate::enhance::field::use_keys;
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
#[derive(Default, Clone)]
@@ -138,3 +136,13 @@ impl IRuntime {
}
}
}
// TODO 完整迁移 enhance 行为后移除
#[inline]
fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
config.iter().filter_map(|(key, _)| key.as_str()).map(|s: &str| {
let mut s: String = s.into();
s.make_ascii_lowercase();
s
})
}
@@ -120,12 +120,6 @@ fn is_binary_admin() -> bool {
.unwrap_or(false)
}
#[inline]
#[cfg(unix)]
pub fn current_gid() -> u32 {
unsafe { libc::getgid() }
}
#[inline]
pub fn list_network_interfaces() -> Vec<String> {
let mut networks = Networks::new();
+13
View File
@@ -69,6 +69,19 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
Our builds and releases run on YXVM dedicated servers that deliver premium resources, strong performance, and high-speed networking. If downloads feel fast and usage feels snappy, it is thanks to robust hardware.
🧩 Highlights of YXVM Dedicated Servers:
- 🌎 Optimized global routes for dramatically faster downloads
- 🔧 Bare-metal resources instead of shared VPS capacity for maximum performance
- 🧠 Great for proxy workloads, hosting web/CDN services, CI/CD pipelines, or any high-load tasks
- 💡 Ready to use instantly with multiple datacenter options, including CN2 and IEPL
- 📦 The configuration used by this project is on sale—feel free to get the same setup
- 🎯 Want the same build environment? [Order a YXVM server today](https://yxvm.com/aff.php?aff=827)
## Features
- Built on high-performance Rust with the Tauri 2 framework
+13
View File
@@ -63,6 +63,19 @@ Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) pa
- Desbloquea servicios de streaming y acceso a ChatGPT
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Patrocinador de la infraestructura de compilación — [Servidores dedicados YXVM](https://yxvm.com/aff.php?aff=827)
Las compilaciones y lanzamientos del proyecto se ejecutan en servidores dedicados de YXVM, que proporcionan recursos premium, alto rendimiento y redes de alta velocidad. Si las descargas son rápidas y el uso es fluido, es gracias a este hardware robusto.
🧩 Ventajas de los servidores dedicados YXVM:
- 🌎 Rutas globales optimizadas para descargas significativamente más rápidas
- 🔧 Recursos bare-metal, en lugar de VPS compartidos, para obtener el máximo rendimiento
- 🧠 Ideales para proxys, alojamiento de sitios web/CDN, pipelines de CI/CD o cualquier carga elevada
- 💡 Listos para usar al instante, con múltiples centros de datos disponibles (incluidos CN2 e IEPL)
- 📦 La misma configuración utilizada por este proyecto está disponible para su compra
- 🎯 ¿Quieres el mismo entorno de compilación? [Solicita un servidor YXVM hoy](https://yxvm.com/aff.php?aff=827)
## Funciones
- Basado en Rust de alto rendimiento y en el framework Tauri 2
+12
View File
@@ -62,6 +62,18 @@
- پشتیبانی از سرویس‌های استریم و دسترسی به ChatGPT
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### حامی زیرساخت ساخت — [سرورهای اختصاصی YXVM](https://yxvm.com/aff.php?aff=827)
بیلدها و نسخه‌های ما روی سرورهای اختصاصی YXVM اجرا می‌شوند که منابع ممتاز، عملکرد قوی و شبکه پرسرعت را ارائه می‌دهند. اگر دانلودها سریع و استفاده از آن سریع به نظر می‌رسد، به لطف سخت‌افزار قوی است.
🧩 نکات برجسته سرورهای اختصاصی YXVM:
- 🌎 مسیرهای جهانی بهینه شده برای دانلودهای بسیار سریعتر
- 🔧 منابع فیزیکی به جای ظرفیت VPS مشترک برای حداکثر کارایی
- 🧠 عالی برای بارهای کاری پروکسی، میزبانی سرویس‌های وب/CDN، خطوط لوله CI/CD یا هرگونه کار با بار بالا
- 💡 آماده استفاده فوری با گزینه‌های متعدد مرکز داده، از جمله CN2 و IEPL
- 📦 پیکربندی مورد استفاده در این پروژه در حال فروش است - می‌توانید همان تنظیمات را تهیه کنید.
- 🎯 آیا محیط ساخت مشابهی می‌خواهید؟ [همین امروز یک سرور YXVM سفارش دهید](https://yxvm.com/aff.php?aff=827)
## ویژگی‌ها
- ساخته شده بر اساس Rust با کارایی بالا و فریم‌ورک Tauri 2
+13
View File
@@ -63,6 +63,19 @@ Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポー
- ストリーミングおよび ChatGPT の利用にも対応
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### ビルド環境スポンサー — [YXVM 専用サーバー](https://yxvm.com/aff.php?aff=827)
本プロジェクトのビルドとリリースは、YXVM の専用サーバーによって支えられています。高速ダウンロードや快適な操作性は、強力なハードウェアがあってこそです。
🧩 YXVM 専用サーバーの特長:
- 🌎 最適化されたグローバル回線で圧倒的なダウンロード速度
- 🔧 VPS とは異なるベアメタル資源で最高性能を発揮
- 🧠 プロキシ運用、Web/CDN ホスティング、CI/CD など高負荷ワークロードに最適
- 💡 複数データセンターから即時利用可能。CN2 や IEPL も選択可
- 📦 本プロジェクトが使用している構成も販売中。同じ環境を入手できます
- 🎯 同じビルド体験をしたい方は [今すぐ YXVM サーバーを注文](https://yxvm.com/aff.php?aff=827)
## 機能
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
+13
View File
@@ -63,6 +63,19 @@ Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니
- 스트리밍 및 ChatGPT 접근 지원
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
🧩 YXVM 전용 서버 하이라이트:
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
## 기능
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
+13
View File
@@ -59,6 +59,19 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
- Разблокировка потоковые сервисы и ChatGPT
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### Среда сборки и публикации этого проекта полностью поддерживается выделенным сервером [YXVM](https://yxvm.com/aff.php?aff=827)
Благодарим вас за предоставление надежной бэкэнд-среды с эксклюзивными ресурсами, высокой производительностью и высокоскоростной сетью. Если вы считаете, что загрузка файлов происходит достаточно быстро, а использование — достаточно плавно, то это потому, что мы используем серверы высшего уровня!
🧩 Преимущества выделенного сервера YXVM:
- 🌎 Премиум-сеть с оптимизацией обратного пути для молниеносной скорости загрузки
- 🔧 Выделенные физические серверные ресурсы, не имеющие аналогов среди VPS, обеспечивающие максимальную производительность
- 🧠 Идеально подходит для прокси, хостинга веб-сайтов/CDN-сайтов, рабочих процессов CI/CD или любых приложений с высокой нагрузкой
- 💡 Поддержка использования сразу после включения, выбор нескольких дата-центров, CN2 / IEPL на выбор
- 📦 Эта конфигурация в настоящее время доступна для покупки — не стесняйтесь заказывать ту же модель!
- 🎯 Хотите попробовать такую же сборку? [Закажите выделенный сервер YXVM прямо сейчас!](https://yxvm.com/aff.php?aff=827)
## Фичи
- Основан на произвоительном Rust и фреймворке Tauri 2
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.4.5",
"version": "2.4.5-rc.1",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -48,9 +48,9 @@
"@tanstack/react-virtual": "^3.13.18",
"@tauri-apps/api": "2.9.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-dialog": "^2.5.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-http": "~2.5.6",
"@tauri-apps/plugin-http": "~2.5.5",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "2.3.4",
"@tauri-apps/plugin-updater": "2.9.0",
@@ -66,9 +66,9 @@
"nanoid": "^5.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-error-boundary": "6.1.0",
"react-hook-form": "^7.71.1",
"react-i18next": "16.5.3",
"react-error-boundary": "6.0.3",
"react-hook-form": "^7.71.0",
"react-i18next": "16.5.2",
"react-markdown": "10.1.0",
"react-router": "^7.12.0",
"react-virtuoso": "^4.18.1",
@@ -79,12 +79,12 @@
},
"devDependencies": {
"@actions/github": "^7.0.0",
"@eslint-react/eslint-plugin": "^2.7.2",
"@eslint-react/eslint-plugin": "^2.5.5",
"@eslint/js": "^9.39.2",
"@tauri-apps/cli": "2.9.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.9",
"@types/node": "^24.10.7",
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-legacy": "^7.2.1",
@@ -97,7 +97,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
@@ -109,12 +109,12 @@
"jiti": "^2.6.1",
"lint-staged": "^16.2.7",
"node-fetch": "^3.3.2",
"prettier": "^3.8.0",
"prettier": "^3.7.4",
"sass": "^1.97.2",
"tar": "^7.5.3",
"terser": "^5.46.0",
"tar": "^7.5.2",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0",
"typescript-eslint": "^8.52.0",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0"
},
+791 -791
View File
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.4.5"
version = "2.4.5-rc.1"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -34,6 +34,7 @@ tauri-build = { version = "2.5.3", features = [] }
clash-verge-draft = { workspace = true }
clash-verge-logging = { workspace = true }
clash-verge-signal = { workspace = true }
clash-verge-types = { workspace = true }
clash-verge-i18n = { workspace = true }
tauri-plugin-clash-verge-sysinfo = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
@@ -59,7 +60,7 @@ warp = { version = "0.4.2", features = ["server"] }
open = "5.3.3"
dunce = "1.0.5"
nanoid = "0.4"
chrono = "0.4.43"
chrono = "0.4.42"
boa_engine = "0.21.0"
once_cell = { version = "1.21.3", features = ["parking_lot"] }
delay_timer = "0.11.6"
@@ -71,18 +72,18 @@ reqwest = { version = "0.13.1", features = [
"form",
] }
regex = "1.12.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "0.4.3", features = [
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
"guard",
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.3.4"
tauri-plugin-dialog = "2.6.0"
tauri-plugin-dialog = "2.5.0"
tauri-plugin-fs = "2.4.5"
tauri-plugin-process = "2.3.1"
tauri-plugin-deep-link = "2.4.6"
tauri-plugin-window-state = "2.4.1"
zip = "7.1.0"
reqwest_dav = "0.3.1"
zip = "7.0.0"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.4"
@@ -92,13 +93,13 @@ scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.3"
tokio-stream = "0.1.18"
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2.5.6"
tauri-plugin-http = "2.5.5"
console-subscriber = { version = "0.5.0", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
clash_verge_service_ipc = { version = "2.1.1", features = [
clash_verge_service_ipc = { version = "2.0.29", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.8.0"
+1 -1
View File
@@ -117,7 +117,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件");
logging!(debug, Type::Cmd, "重新排序配置文件");
Config::profiles().await.apply();
Ok(())
}
+2 -1
View File
@@ -1,6 +1,6 @@
use super::{IClashTemp, IProfiles, IVerge};
use crate::{
config::{PrfItem, profiles_append_item_safe, runtime::IRuntime},
config::{PrfItem, profiles_append_item_safe},
constants::{files, timing},
core::{
CoreManager,
@@ -16,6 +16,7 @@ use anyhow::{Result, anyhow};
use backoff::{Error as BackoffError, ExponentialBackoff};
use clash_verge_draft::Draft;
use clash_verge_logging::{Type, logging, logging_error};
use clash_verge_types::runtime::IRuntime;
use smartstring::alias::String;
use std::path::PathBuf;
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
-1
View File
@@ -4,7 +4,6 @@ mod config;
mod encrypt;
mod prfitem;
pub mod profiles;
pub mod runtime;
mod verge;
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*};
+18 -21
View File
@@ -60,13 +60,13 @@ pub struct PrfItem {
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct PrfSelected {
pub name: Option<String>,
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
pub struct PrfExtra {
pub upload: u64,
pub download: u64,
@@ -124,25 +124,22 @@ pub struct PrfOption {
impl PrfOption {
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
match (one, other) {
(Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone();
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
result.danger_accept_invalid_certs =
b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs);
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
result.update_interval = b_ref.update_interval.or(result.update_interval);
result.merge = b_ref.merge.clone().or(result.merge);
result.script = b_ref.script.clone().or(result.script);
result.rules = b_ref.rules.clone().or(result.rules);
result.proxies = b_ref.proxies.clone().or(result.proxies);
result.groups = b_ref.groups.clone().or(result.groups);
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
Some(result)
}
(Some(a_ref), None) => Some(a_ref.clone()),
(None, Some(b_ref)) => Some(b_ref.clone()),
(Some(a), Some(b)) => Some(Self {
user_agent: b.user_agent.as_ref().or(a.user_agent.as_ref()).cloned(),
with_proxy: b.with_proxy.or(a.with_proxy),
self_proxy: b.self_proxy.or(a.self_proxy),
danger_accept_invalid_certs: b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs),
allow_auto_update: b.allow_auto_update.or(a.allow_auto_update),
update_interval: b.update_interval.or(a.update_interval),
merge: b.merge.as_ref().or(a.merge.as_ref()).cloned(),
script: b.script.as_ref().or(a.script.as_ref()).cloned(),
rules: b.rules.as_ref().or(a.rules.as_ref()).cloned(),
proxies: b.proxies.as_ref().or(a.proxies.as_ref()).cloned(),
groups: b.groups.as_ref().or(a.groups.as_ref()).cloned(),
timeout_seconds: b.timeout_seconds.or(a.timeout_seconds),
}),
(Some(a), None) => Some(a.clone()),
(None, Some(b)) => Some(b.clone()),
(None, None) => None,
}
}
+226 -248
View File
@@ -5,12 +5,16 @@ use crate::utils::{
};
use anyhow::{Context as _, Result, bail};
use clash_verge_logging::{Type, logging};
use once_cell::sync::OnceCell;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use tokio::fs;
static PROFILE_FILE_RE: OnceCell<Regex> = OnceCell::new();
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IProfiles {
@@ -37,23 +41,15 @@ pub struct CleanupResult {
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key.to_owned();
if let Some(ref val) = $rv.$key {
if Some(val) != $lv.$key.as_ref() {
$lv.$key = Some(val.to_owned());
}
}
};
}
impl IProfiles {
// Helper to find and remove an item by uid from the items vec, returning its file name (if any).
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<String>) -> Option<String> {
for (i, _) in items.iter().enumerate() {
if items[i].uid == target_uid {
return items.remove(i).file;
}
}
None
}
pub async fn new() -> Self {
let path = match dirs::profiles_path() {
Ok(p) => p,
@@ -63,21 +59,22 @@ impl IProfiles {
}
};
match help::read_yaml::<Self>(&path).await {
Ok(mut profiles) => {
let items = profiles.items.get_or_insert_with(Vec::new);
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
}
}
profiles
}
let mut profiles = match help::read_yaml::<Self>(&path).await {
Ok(profiles) => profiles,
Err(err) => {
logging!(error, Type::Config, "{err}");
Self::default()
return Self::default();
}
};
let items = profiles.items.get_or_insert_with(Vec::new);
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
}
}
profiles
}
pub async fn save_file(&self) -> Result<()> {
@@ -113,38 +110,28 @@ impl IProfiles {
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
let uid_str = uid.as_ref();
if let Some(items) = self.items.as_ref() {
for each in items.iter() {
if let Some(uid_val) = &each.uid
&& uid_val.as_str() == uid_str
{
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{}\"", uid_str);
self.items
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no profile items found"))?
.iter()
.find(|each| each.uid.as_ref().is_some_and(|uid_val| uid_val.as_str() == uid_str))
.ok_or_else(|| anyhow::anyhow!("failed to get the profile item \"uid:{}\"", uid_str))
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
let uid = &item.uid;
if uid.is_none() {
bail!("the uid should not be null");
}
anyhow::ensure!(item.uid.is_some(), "the uid should not be null");
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
if item.file.is_none() {
bail!("the file should not be null");
}
anyhow::ensure!(item.file.is_some(), "the file should not be null");
let file = item
.file
.clone()
.as_ref()
.ok_or_else(|| anyhow::anyhow!("file field is required when file_data is provided"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
@@ -153,111 +140,116 @@ impl IProfiles {
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
if self.current.is_none() && (item.itype == Some("remote".into()) || item.itype == Some("local".into())) {
self.current = uid.to_owned();
if self.current.is_none()
&& let Some(t) = item.itype.as_deref()
&& (t == "remote" || t == "local")
{
self.current = item.uid.to_owned();
}
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(items) = self.items.as_mut() {
items.push(item.to_owned());
}
self.items.get_or_insert_default().push(std::mem::take(item));
Ok(())
}
/// reorder items
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
pub async fn reorder(&mut self, active_id: &str, over_id: &str) -> Result<()> {
if active_id == over_id {
return Ok(());
}
for (i, _) in items.iter().enumerate() {
if items[i].uid.as_ref() == Some(active_id) {
old_index = Some(i);
let Some(items) = self.items.as_mut() else {
return Ok(());
};
let mut old_idx = None;
let mut new_idx = None;
for (i, item) in items.iter().enumerate() {
if let Some(uid) = item.uid.as_ref() {
if uid == active_id {
old_idx = Some(i);
}
if uid == over_id {
new_idx = Some(i);
}
}
if items[i].uid.as_ref() == Some(over_id) {
new_index = Some(i);
if old_idx.is_some() && new_idx.is_some() {
break;
}
}
let (old_idx, new_idx) = match (old_index, new_index) {
(Some(old), Some(new)) => (old, new),
_ => return Ok(()),
};
let item = items.remove(old_idx);
items.insert(new_idx, item);
self.items = Some(items);
self.save_file().await
if let (Some(old), Some(new)) = (old_idx, new_idx) {
if old < new {
items[old..=new].rotate_left(1);
} else {
items[new..=old].rotate_right(1);
}
return self.save_file().await;
}
Ok(())
}
/// update the item value
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let items = self
.items
.as_mut()
.ok_or_else(|| anyhow::anyhow!("no profile items found"))?;
for each in items.iter_mut() {
if each.uid.as_ref() == Some(uid) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
let target = items.iter_mut().find(|each| each.uid.as_ref() == Some(uid));
self.items = Some(items);
return self.save_file().await;
}
if let Some(each) = target {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
return self.save_file().await;
}
self.items = Some(items);
bail!("failed to find the profile item \"uid:{uid}\"")
}
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
let target = self
.items
.get_or_insert_default()
.iter_mut()
.find(|each| each.uid.as_ref() == Some(uid))
.ok_or_else(|| anyhow::anyhow!("Item not found"))?;
// find the item
let _ = self.get_item(uid)?;
target.extra = item.extra;
target.updated = item.updated;
target.home = std::mem::take(&mut item.home);
target.option = PrfOption::merge(target.option.as_ref(), item.option.as_ref());
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
let Some(file_data) = item.file_data.take() else {
return self.save_file().await;
};
for each in items.iter_mut() {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home.to_owned();
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
let file = each.file.take();
let file =
file.unwrap_or_else(|| item.file.take().unwrap_or_else(|| format!("{}.yaml", &uid).into()));
let file = target
.file
.take()
.or_else(|| item.file.take())
.unwrap_or_else(|| format!("{}.yaml", uid).into());
// the file must exists
each.file = Some(file.clone());
let path = dirs::app_profiles_dir()?.join(file.as_str());
let path = dirs::app_profiles_dir()?.join(file.as_str());
fs::write(&path, file_data.as_bytes())
.await
.with_context(|| format!("failed to write to file \"{file}\""))?;
fs::write(&path, file_data.as_bytes())
.await
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
break;
}
}
}
target.file = Some(file);
self.save_file().await
}
@@ -265,68 +257,82 @@ impl IProfiles {
/// delete item
/// if delete the current then return true
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(uid);
let current = current.clone();
let item = self.get_item(uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
let mut items = self.items.take().unwrap_or_default();
let uids_to_remove: HashSet<String> = {
let item = self.get_item(uid)?;
let mut set = HashSet::new();
set.insert(uid.clone());
// remove the main item (if exists) and delete its file
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
// remove related extension items (merge, script, rules, proxies, groups)
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
}
// delete the original uid
if current == *uid {
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
self.current = item.uid.clone();
break;
if let Some(opt) = &item.option {
if let Some(u) = &opt.merge {
set.insert(u.clone());
}
if let Some(u) = &opt.script {
set.insert(u.clone());
}
if let Some(u) = &opt.rules {
set.insert(u.clone());
}
if let Some(u) = &opt.proxies {
set.insert(u.clone());
}
if let Some(u) = &opt.groups {
set.insert(u.clone());
}
}
set
};
let mut items = self.items.take().unwrap_or_default();
let mut deleted_files = Vec::new();
items.retain_mut(|item| {
if let Some(item_uid) = item.uid.as_ref()
&& uids_to_remove.contains(item_uid)
{
if let Some(file) = item.file.take() {
deleted_files.push(file);
}
return false;
}
true
});
let is_deleting_current = self.current.as_ref() == Some(uid);
if is_deleting_current {
self.current = items
.iter()
.find(|i| i.itype.as_deref() == Some("remote") || i.itype.as_deref() == Some("local"))
.and_then(|i| i.uid.clone());
}
self.items = Some(items);
if let Ok(profile_dir) = dirs::app_profiles_dir() {
for file in deleted_files {
let _ = profile_dir.join(file.as_str()).remove_if_exists().await;
}
}
self.save_file().await?;
Ok(current == *uid)
Ok(is_deleting_current)
}
/// 获取current指向的订阅内容
pub async fn current_mapping(&self) -> Result<Mapping> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let file_path = match item.file.as_ref() {
Some(file) => dirs::app_profiles_dir()?.join(file.as_str()),
None => bail!("failed to get the file field"),
};
return help::read_mapping(&file_path).await;
}
bail!("failed to find the current profile \"uid:{current}\"");
}
_ => Ok(Mapping::new()),
}
let (Some(current), Some(items)) = (self.current.as_ref(), self.items.as_ref()) else {
return Ok(Mapping::new());
};
let Some(target) = items.iter().find(|e| e.uid.as_ref() == Some(current)) else {
bail!("failed to find the current profile \"uid:{current}\"");
};
let file = target
.file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("failed to get the file field"))?;
let file_path = dirs::app_profiles_dir()?.join(file.as_str());
help::read_mapping(&file_path).await
}
/// 判断profile是否是current指向的
@@ -336,32 +342,32 @@ impl IProfiles {
/// 获取所有的profiles(uid,名称, 是否为 current)
pub fn profiles_preview(&self) -> Option<Vec<IProfilePreview<'_>>> {
self.items.as_ref().map(|items| {
items
.iter()
.filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
let is_current = self.is_current_profile_index(uid);
let preview = IProfilePreview { uid, name, is_current };
Some(preview)
} else {
None
}
let items = self.items.as_ref()?;
let current_uid = self.current.as_ref();
let previews = items
.iter()
.filter_map(|e| {
let uid = e.uid.as_ref()?;
let name = e.name.as_ref()?;
Some(IProfilePreview {
uid,
name,
is_current: current_uid == Some(uid),
})
.collect()
})
})
.collect();
Some(previews)
}
/// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
if let Some(items) = &self.items {
for item in items {
if item.uid.as_ref() == Some(uid) {
return item.name.as_ref();
}
}
}
None
pub fn get_name_by_uid(&self, uid: &str) -> Option<&String> {
self.items
.as_ref()?
.iter()
.find(|item| item.uid.as_deref() == Some(uid))
.and_then(|item| item.name.as_ref())
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
@@ -452,59 +458,39 @@ impl IProfiles {
/// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<&str> {
let mut active_files: HashSet<&str> = HashSet::new();
let mut active_files = HashSet::new();
let items = match &self.items {
Some(i) => i,
None => return active_files,
};
if let Some(items) = &self.items {
for item in items {
// 收集所有类型 profile 的文件
if let Some(file) = &item.file {
active_files.insert(file);
}
let item_map: HashMap<Option<&str>, &PrfItem> = items.iter().map(|i| (i.uid.as_deref(), i)).collect();
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype
&& (itype == "remote" || itype == "local")
&& let Some(option) = &item.option
for item in items {
if let Some(f) = &item.file {
active_files.insert(f.as_str());
}
let Some(opt) = &item.option else {
continue;
};
let related = [
opt.merge.as_deref(),
opt.script.as_deref(),
opt.rules.as_deref(),
opt.proxies.as_deref(),
opt.groups.as_deref(),
];
for r_uid in related.into_iter().flatten() {
if let Some(r_item) = item_map.get(&Some(r_uid))
&& let Some(f) = &r_item.file
{
// 收集关联的扩展文件
if let Some(merge_uid) = &option.merge
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file);
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file);
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file);
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file);
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file);
}
active_files.insert(f.as_str());
}
}
}
active_files
}
@@ -519,18 +505,10 @@ impl IProfiles {
// p12345678.yaml (proxies)
// g12345678.yaml (groups)
let patterns = [
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
r"^s[a-zA-Z0-9]+\.js$", // Script files
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
];
patterns.iter().any(|pattern| {
regex::Regex::new(pattern)
.map(|re| re.is_match(filename))
.unwrap_or(false)
})
#[allow(clippy::unwrap_used)]
let re = PROFILE_FILE_RE
.get_or_init(|| Regex::new(r"^(?:[RLmprg][a-zA-Z0-9_-]+\.yaml|s[a-zA-Z0-9_-]+\.js)$").unwrap());
re.is_match(filename)
}
}
+3 -2
View File
@@ -14,6 +14,7 @@ use std::{
sync::Arc,
time::Duration,
};
use tauri_plugin_http::reqwest as tauri_reqwest;
use tokio::{fs, time::timeout};
use zip::write::SimpleFileOptions;
@@ -110,12 +111,12 @@ impl WebDavClient {
// 创建新的客户端
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
tauri_reqwest::Client::builder()
.use_rustls_tls()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
.redirect(reqwest::redirect::Policy::custom(|attempt| {
.redirect(tauri_reqwest::redirect::Policy::custom(|attempt| {
// 允许所有请求类型的重定向,包括PUT
if attempt.previous().len() >= 5 {
attempt.error("重定向次数过多")
+2 -1
View File
@@ -1,12 +1,13 @@
use super::CoreManager;
use crate::{
config::{Config, ConfigType, runtime::IRuntime},
config::{Config, ConfigType},
constants::timing,
core::{handle, validate::CoreConfigValidator},
utils::{dirs, help},
};
use anyhow::{Result, anyhow};
use clash_verge_logging::{Type, logging};
use clash_verge_types::runtime::IRuntime;
use smartstring::alias::String;
use std::{collections::HashSet, path::PathBuf, time::Instant};
use tauri_plugin_mihomo::Error as MihomoError;
+3 -10
View File
@@ -256,11 +256,9 @@ fn install_service() -> Result<()> {
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
let gid = tauri_plugin_clash_verge_sysinfo::current_gid();
let prompt = clash_verge_i18n::t!("service.adminInstallPrompt");
let command = format!(
r#"do shell script "sudo CLASH_VERGE_SERVICE_GID={gid} '{install_shell}'" with administrator privileges with prompt "{prompt}""#
);
let command =
format!(r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#);
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
@@ -383,12 +381,7 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
/// 检查服务是否正在运行
pub async fn is_service_available() -> Result<()> {
if let Err(e) = Path::metadata(clash_verge_service_ipc::IPC_PATH.as_ref()) {
let verge = Config::verge().await;
let verge_last = verge.latest_arc();
let is_enable = verge_last.enable_tun_mode.unwrap_or(false);
if is_enable {
logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e);
}
logging!(warn, Type::Service, "Some issue with service IPC Path: {}", e);
return Err(e.into());
}
clash_verge_service_ipc::connect().await?;
+1
View File
@@ -25,6 +25,7 @@ pub struct TimerTask {
pub last_run: i64, // Timestamp of last execution
}
// TODO 一个 Timer 负责轻量, 一个 Timer 负责订阅更新。当前会生产 N(订阅数量) + 1 个定时任务
pub struct Timer {
/// cron manager
pub delay_timer: Arc<RwLock<DelayTimer>>,
+10 -7
View File
@@ -61,11 +61,14 @@ pub fn use_sort(config: Mapping) -> Mapping {
ret
}
#[inline]
pub fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
config.iter().filter_map(|(key, _)| key.as_str()).map(|s: &str| {
let mut s: String = s.into();
s.make_ascii_lowercase();
s
})
pub fn use_keys(config: &Mapping) -> Vec<String> {
config
.iter()
.filter_map(|(key, _)| key.as_str())
.map(|s: &str| {
let mut s: String = s.into();
s.make_ascii_lowercase();
s
})
.collect()
}
+1 -1
View File
@@ -310,7 +310,7 @@ fn process_global_items(
profile_name: &String,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
let mut result_map = HashMap::new();
let mut exists_keys = use_keys(&config).collect::<Vec<_>>();
let mut exists_keys = use_keys(&config);
if let ChainType::Merge(merge) = global_merge.data {
exists_keys.extend(use_keys(&merge));
+3 -2
View File
@@ -19,6 +19,7 @@ use crate::{
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use once_cell::sync::OnceCell;
use std::time::Duration;
use tauri::{AppHandle, Manager as _};
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
@@ -60,11 +61,11 @@ mod app_init {
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
.pool_config(
tauri_plugin_mihomo::IpcPoolConfigBuilder::new()
.min_connections(3)
.min_connections(1)
.max_connections(32)
.idle_timeout(std::time::Duration::from_secs(60))
.health_check_interval(std::time::Duration::from_secs(60))
.reject_policy(RejectPolicy::Wait)
.reject_policy(RejectPolicy::Timeout(Duration::from_secs(3)))
.build(),
)
.build(),
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.4.5",
"version": "2.4.5-rc.1",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
+2 -5
View File
@@ -2,18 +2,15 @@ import { ReactNode } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
function ErrorFallback({ error }: FallbackProps) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
return (
<div role="alert" style={{ padding: 16 }}>
<h4>Something went wrong:(</h4>
<pre>{errorMessage}</pre>
<pre>{error.message}</pre>
<details title="Error Stack">
<summary>Error Stack</summary>
<pre>{errorStack}</pre>
<pre>{error.stack}</pre>
</details>
</div>
);
+15 -2
View File
@@ -4,7 +4,13 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useClash } from "@/hooks/use-clash";
import { useAppData } from "@/providers/app-data-context";
import {
useAppUptime,
useClashConfig,
useRulesData,
useSystemProxyAddress,
useSystemProxyData,
} from "@/hooks/use-clash-data";
import { EnhancedCard } from "./enhanced-card";
@@ -19,7 +25,14 @@ const formatUptime = (uptimeMs: number) => {
export const ClashInfoCard = () => {
const { t } = useTranslation();
const { version: clashVersion } = useClash();
const { clashConfig, rules, uptime, systemProxyAddress } = useAppData();
const { clashConfig } = useClashConfig();
const { sysproxy } = useSystemProxyData();
const { rules } = useRulesData();
const { uptime } = useAppUptime();
const systemProxyAddress = useSystemProxyAddress({
clashConfig,
sysproxy,
});
// 使用useMemo缓存格式化后的uptime,避免频繁计算
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
+2 -2
View File
@@ -9,8 +9,8 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useClashConfig } from "@/hooks/use-clash-data";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { patchClashMode } from "@/services/cmds";
import type { TranslationKey } from "@/types/generated/i18n-keys";
@@ -41,7 +41,7 @@ const MODE_META: Record<
export const ClashModeCard = () => {
const { t } = useTranslation();
const { verge } = useVerge();
const { clashConfig, refreshClashConfig } = useAppData();
const { clashConfig, refreshClashConfig } = useClashConfig();
// 支持的模式列表
const modeList = CLASH_MODES;
+8 -2
View File
@@ -34,10 +34,14 @@ import { useNavigate } from "react-router";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
import { EnhancedCard } from "@/components/home/enhanced-card";
import {
useClashConfig,
useProxiesData,
useRulesData,
} from "@/hooks/use-clash-data";
import { useProfiles } from "@/hooks/use-profiles";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import delayManager from "@/services/delay";
import { debugLog } from "@/utils/debug";
@@ -101,7 +105,9 @@ export const CurrentProxyCard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const { proxies, clashConfig, refreshProxy, rules } = useAppData();
const { proxies, refreshProxy } = useProxiesData();
const { clashConfig } = useClashConfig();
const { rules } = useRulesData();
const { verge } = useVerge();
const { current: currentProfile } = useProfiles();
const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false;
+2 -2
View File
@@ -24,7 +24,7 @@ import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { useAppData } from "@/providers/app-data-context";
import { useRefreshAll } from "@/hooks/use-clash-data";
import { openWebUrl, updateProfile } from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import parseTraffic from "@/utils/parse-traffic";
@@ -281,7 +281,7 @@ export const HomeProfileCard = ({
}: HomeProfileCardProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { refreshAll } = useAppData();
const refreshAll = useRefreshAll();
// 更新当前订阅
const [updating, setUpdating] = useState(false);
+5 -4
View File
@@ -22,7 +22,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { useProxiesData, useProxyProvidersData } from "@/hooks/use-clash-data";
import { showNotice } from "@/services/notice-service";
import parseTraffic from "@/utils/parse-traffic";
@@ -48,7 +48,8 @@ const parseExpire = (expire?: number) => {
export const ProviderButton = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
const { proxyProviders, refreshProxyProviders } = useProxyProvidersData();
const { refreshProxy } = useProxiesData();
const [updating, setUpdating] = useState<Record<string, boolean>>({});
// 检查是否有提供者
@@ -175,8 +176,8 @@ export const ProviderButton = () => {
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {})
.sort()
.map(([key, item]) => {
const provider = item;
.map(([key, provider]) => {
if (!provider) return null;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
+3 -3
View File
@@ -38,7 +38,7 @@ import {
selectNodeForGroup,
} from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { useProxiesData } from "@/hooks/use-clash-data";
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
import { debugLog } from "@/utils/debug";
@@ -199,7 +199,7 @@ export const ProxyChain = ({
}: ProxyChainProps) => {
const theme = useTheme();
const { t } = useTranslation();
const { proxies, refreshProxy } = useAppData();
const { proxies, refreshProxy } = useProxiesData();
const [isConnecting, setIsConnecting] = useState(false);
const markUnsavedChanges = useCallback(() => {
onMarkUnsavedChanges?.();
@@ -221,7 +221,7 @@ export const ProxyChain = ({
}
const proxyChainGroup = proxies.groups.find(
(group: { name: string }) => group.name === selectedGroup,
(group) => group.name === selectedGroup,
);
return proxyChainGroup?.now === lastNode.name;
+2 -2
View File
@@ -16,9 +16,9 @@ import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
import { BaseEmpty } from "@/components/base";
import { useProxiesData } from "@/hooks/use-clash-data";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
import delayManager from "@/services/delay";
import { debugLog } from "@/utils/debug";
@@ -80,7 +80,7 @@ export const ProxyGroups = (props: Props) => {
}>({ open: false, message: "" });
const { verge } = useVerge();
const { proxies: proxiesData } = useAppData();
const { proxies: proxiesData } = useProxiesData();
const groups = proxiesData?.groups;
const availableGroups = useMemo(() => {
if (!groups) return [];
+4 -20
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo } from "react";
import useSWR from "swr";
import { useProxiesData } from "@/hooks/use-clash-data";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { getRuntimeConfig } from "@/services/cmds";
import delayManager from "@/services/delay";
import { debugLog } from "@/utils/debug";
@@ -34,24 +34,8 @@ interface IProxyItem {
}
// 代理组类型
type ProxyGroup = {
name: string;
type: string;
udp: boolean;
xudp: boolean;
tfo: boolean;
mptcp: boolean;
smux: boolean;
history: {
time: string;
delay: number;
}[];
now: string;
all: IProxyItem[];
hidden?: boolean;
icon?: string;
testUrl?: string;
provider?: string;
type ProxyGroup = IProxyGroupItem & {
now?: string;
};
export interface IRenderItem {
@@ -100,7 +84,7 @@ export const useRenderList = (
selectedGroup?: string | null,
) => {
// 使用全局数据提供者
const { proxies: proxiesData, refreshProxy } = useAppData();
const { proxies: proxiesData, refreshProxy } = useProxiesData();
const { verge } = useVerge();
const { width } = useWindowWidth();
const [headStates, setHeadState] = useHeadStateNew();
+20 -5
View File
@@ -21,7 +21,10 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import type {
useRuleProvidersData,
useRulesData,
} from "@/hooks/use-clash-data";
import { showNotice } from "@/services/notice-service";
// 辅助组件 - 类型框
@@ -37,10 +40,22 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
lineHeight: 1.25,
}));
export const ProviderButton = () => {
type RuleProvidersHook = ReturnType<typeof useRuleProvidersData>;
type RulesHook = ReturnType<typeof useRulesData>;
interface ProviderButtonProps {
ruleProviders: RuleProvidersHook["ruleProviders"];
refreshRuleProviders: RuleProvidersHook["refreshRuleProviders"];
refreshRules: RulesHook["refreshRules"];
}
export const ProviderButton = ({
ruleProviders,
refreshRuleProviders,
refreshRules,
}: ProviderButtonProps) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData();
const [updating, setUpdating] = useState<Record<string, boolean>>({});
// 检查是否有提供者
@@ -163,8 +178,8 @@ export const ProviderButton = () => {
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(ruleProviders || {})
.sort()
.map(([key, item]) => {
const provider = item;
.map(([key, provider]) => {
if (!provider) return null;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
@@ -79,7 +79,6 @@ export const BackupHistoryViewer = ({
const { verge } = useVerge();
const [rows, setRows] = useState<BackupRow[]>([]);
const [loading, setLoading] = useState(false);
const [isRestoring, setIsRestoring] = useState(false);
const [isRestarting, setIsRestarting] = useState(false);
const isLocal = source === "local";
const isWebDavConfigured = Boolean(
@@ -87,7 +86,7 @@ export const BackupHistoryViewer = ({
);
const shouldSkipWebDav = !isLocal && !isWebDavConfigured;
const pageSize = 8;
const isBusy = loading || isRestoring || isRestarting;
const isBusy = loading || isRestarting;
const buildRow = useCallback(
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
@@ -196,32 +195,24 @@ export const BackupHistoryViewer = ({
});
const handleRestore = useLockFn(async (filename: string) => {
if (isRestoring || isRestarting) return;
if (isRestarting) return;
if (
!(await confirmAsync(t("settings.modals.backup.messages.confirmRestore")))
)
return;
setIsRestoring(true);
try {
if (isLocal) {
await restoreLocalBackup(filename);
} else {
await restoreWebDavBackup(filename);
}
showNotice.success("settings.modals.backup.messages.restoreSuccess");
setIsRestarting(true);
window.setTimeout(() => {
void restartApp().catch((err: unknown) => {
setIsRestarting(false);
showNotice.error(err);
});
}, 1000);
} catch (error) {
console.error(error);
showNotice.error(error);
} finally {
setIsRestoring(false);
if (isLocal) {
await restoreLocalBackup(filename);
} else {
await restoreWebDavBackup(filename);
}
showNotice.success("settings.modals.backup.messages.restoreSuccess");
setIsRestarting(true);
window.setTimeout(() => {
void restartApp().catch((err: unknown) => {
setIsRestarting(false);
showNotice.error(err);
});
}, 1000);
});
const handleExport = useLockFn(async (filename: string) => {
@@ -112,7 +112,6 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
close: () => setOpen(false),
}));
// TODO 减少代码复杂度,性能开支
const onSave = useLockFn(async () => {
// 端口冲突检测
const portList = [
@@ -141,26 +140,14 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
return;
}
const original = originalPortsRef.current;
const changedPorts: number[] = [];
if (mixedPort !== original?.mixedPort) changedPorts.push(mixedPort);
if (socksEnabled && socksPort !== original?.socksPort)
changedPorts.push(socksPort);
if (httpEnabled && httpPort !== original?.httpPort)
changedPorts.push(httpPort);
if (redirEnabled && redirPort !== original?.redirPort)
changedPorts.push(redirPort);
if (tproxyEnabled && tproxyPort !== original?.tproxyPort)
changedPorts.push(tproxyPort);
for (const port of changedPorts) {
for (const port of portList) {
try {
const inUse = await isPortInUse(port);
if (inUse) {
showNotice.error("settings.modals.clashPort.messages.portInUse", {
port,
});
const original = originalPortsRef.current;
if (original) {
setMixedPort(original.mixedPort);
setSocksPort(original.socksPort);
@@ -214,7 +201,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
};
// 提交保存请求
saveSettings({ clashConfig, vergeConfig });
await saveSettings({ clashConfig, vergeConfig });
});
return (
@@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
import { BaseDialog, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { restartCore } from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
// 定义开发环境的URL列表
@@ -135,7 +134,6 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
),
},
});
await restartCore();
await mutateClash();
},
{
+13 -14
View File
@@ -22,8 +22,7 @@ import {
useState,
} from "react";
import { useTranslation } from "react-i18next";
import useSWR, { mutate } from "swr";
import { getBaseConfig } from "tauri-plugin-mihomo-api";
import { mutate } from "swr";
import {
BaseDialog,
@@ -34,8 +33,12 @@ import {
TooltipIcon,
} from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer";
import {
useClashConfig,
useSystemProxyAddress,
useSystemProxyData,
} from "@/hooks/use-clash-data";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import {
getAutotemProxy,
getNetworkInterfacesInfo,
@@ -107,9 +110,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, patchVerge, mutateVerge } = useVerge();
const [hostOptions, setHostOptions] = useState<string[]>([]);
type SysProxy = Awaited<ReturnType<typeof getSystemProxy>>;
const [sysproxy, setSysproxy] = useState<SysProxy>();
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
@@ -148,12 +148,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
};
const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, {
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000,
});
const { clashConfig } = useClashConfig();
const { sysproxy, refreshSysproxy } = useSystemProxyData();
const prevMixedPortRef = useRef(clashConfig?.mixedPort);
@@ -187,7 +183,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
updateProxy();
}, [clashConfig?.mixedPort, value.pac]);
const { systemProxyAddress } = useAppData();
const systemProxyAddress = useSystemProxyAddress({
clashConfig,
sysproxy,
});
// 为当前状态计算系统代理地址
const getSystemProxyAddress = useMemo(() => {
@@ -237,7 +236,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? "127.0.0.1",
});
getSystemProxy().then((p) => setSysproxy(p));
void refreshSysproxy();
getAutotemProxy().then((p) => setAutoproxy(p));
fetchNetworkInterfaces();
},
+143 -148
View File
@@ -19,162 +19,157 @@ export interface TestViewerRef {
}
// create or edit the test item
export const TestViewer = forwardRef<TestViewerRef, Props>(
({ onChange }, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [openType, setOpenType] = useState<"new" | "edit">("new");
const [loading, setLoading] = useState(false);
const { verge, patchVerge } = useVerge();
const testList = verge?.test_list ?? [];
const { control, ...formIns } = useForm<IVergeTestItem>({
defaultValues: {
name: "",
icon: "",
url: "",
},
export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [openType, setOpenType] = useState<"new" | "edit">("new");
const [loading, setLoading] = useState(false);
const { verge, patchVerge } = useVerge();
const testList = verge?.test_list ?? [];
const { control, ...formIns } = useForm<IVergeTestItem>({
defaultValues: {
name: "",
icon: "",
url: "",
},
});
const patchTestList = async (uid: string, patch: Partial<IVergeTestItem>) => {
const newList = testList.map((x) => {
if (x.uid === uid) {
return { ...x, ...patch };
}
return x;
});
await patchVerge({ test_list: newList });
};
const patchTestList = async (
uid: string,
patch: Partial<IVergeTestItem>,
) => {
const newList = testList.map((x) => {
if (x.uid === uid) {
return { ...x, ...patch };
}
return x;
});
await patchVerge({ test_list: newList });
};
useImperativeHandle(ref, () => ({
create: () => {
setOpenType("new");
setOpen(true);
},
edit: (item) => {
if (item) {
Object.entries(item).forEach(([key, value]) => {
formIns.setValue(key as any, value);
});
}
setOpenType("edit");
setOpen(true);
},
}));
useImperativeHandle(ref, () => ({
create: () => {
setOpenType("new");
setOpen(true);
},
edit: (item) => {
if (item) {
Object.entries(item).forEach(([key, value]) => {
formIns.setValue(key as any, value);
});
}
setOpenType("edit");
setOpen(true);
},
}));
const handleOk = useLockFn(
formIns.handleSubmit(async (form) => {
setLoading(true);
try {
if (!form.name) throw new Error("`Name` should not be null");
if (!form.url) throw new Error("`Url` should not be null");
const handleOk = useLockFn(
formIns.handleSubmit(async (form) => {
setLoading(true);
try {
if (!form.name) throw new Error("`Name` should not be null");
if (!form.url) throw new Error("`Url` should not be null");
let newList;
let uid;
let newList;
let uid;
if (form.icon && form.icon.startsWith("<svg")) {
// 移除 icon 中的注释
if (form.icon) {
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
}
const doc = new DOMParser().parseFromString(
form.icon,
"image/svg+xml",
);
if (doc.querySelector("parsererror")) {
throw new Error("`Icon`svg format error");
}
if (form.icon && form.icon.startsWith("<svg")) {
// 移除 icon 中的注释
if (form.icon) {
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
}
if (openType === "new") {
uid = nanoid();
const item = { ...form, uid };
newList = [...testList, item];
await patchVerge({ test_list: newList });
onChange(uid);
} else {
if (!form.uid) throw new Error("UID not found");
uid = form.uid;
await patchTestList(uid, form);
onChange(uid, form);
const doc = new DOMParser().parseFromString(
form.icon,
"image/svg+xml",
);
if (doc.querySelector("parsererror")) {
throw new Error("`Icon`svg format error");
}
setOpen(false);
setLoading(false);
setTimeout(() => formIns.reset(), 500);
} catch (err: any) {
showNotice.error(err);
setLoading(false);
}
}),
);
const handleClose = () => {
setOpen(false);
setTimeout(() => formIns.reset(), 500);
};
if (openType === "new") {
uid = nanoid();
const item = { ...form, uid };
newList = [...testList, item];
await patchVerge({ test_list: newList });
props.onChange(uid);
} else {
if (!form.uid) throw new Error("UID not found");
uid = form.uid;
const text = {
fullWidth: true,
size: "small",
margin: "normal",
variant: "outlined",
autoComplete: "off",
autoCorrect: "off",
} as const;
return (
<BaseDialog
open={open}
title={
openType === "new"
? t("tests.modals.test.title.create")
: t("tests.modals.test.title.edit")
await patchTestList(uid, form);
props.onChange(uid, form);
}
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={handleClose}
onCancel={handleClose}
onOk={handleOk}
loading={loading}
>
<Controller
name="name"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("shared.labels.name")} />
)}
/>
<Controller
name="icon"
control={control}
render={({ field }) => (
<TextField
{...text}
{...field}
multiline
maxRows={5}
label={t("shared.labels.icon")}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field }) => (
<TextField
{...text}
{...field}
multiline
maxRows={3}
label={t("tests.modals.test.fields.url")}
/>
)}
/>
</BaseDialog>
);
},
);
setOpen(false);
setLoading(false);
setTimeout(() => formIns.reset(), 500);
} catch (err: any) {
showNotice.error(err);
setLoading(false);
}
}),
);
const handleClose = () => {
setOpen(false);
setTimeout(() => formIns.reset(), 500);
};
const text = {
fullWidth: true,
size: "small",
margin: "normal",
variant: "outlined",
autoComplete: "off",
autoCorrect: "off",
} as const;
return (
<BaseDialog
open={open}
title={
openType === "new"
? t("tests.modals.test.title.create")
: t("tests.modals.test.title.edit")
}
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={handleClose}
onCancel={handleClose}
onOk={handleOk}
loading={loading}
>
<Controller
name="name"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("shared.labels.name")} />
)}
/>
<Controller
name="icon"
control={control}
render={({ field }) => (
<TextField
{...text}
{...field}
multiline
maxRows={5}
label={t("shared.labels.icon")}
/>
)}
/>
<Controller
name="url"
control={control}
render={({ field }) => (
<TextField
{...text}
{...field}
multiline
maxRows={3}
label={t("tests.modals.test.fields.url")}
/>
)}
/>
</BaseDialog>
);
});
+206
View File
@@ -0,0 +1,206 @@
import { useCallback, useMemo } from "react";
import useSWR, { useSWRConfig } from "swr";
import {
getBaseConfig,
getRuleProviders,
getRules,
} from "tauri-plugin-mihomo-api";
import {
calcuProxies,
calcuProxyProviders,
getAppUptime,
getSystemProxy,
} from "@/services/cmds";
import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config";
import { useSharedSWRPoller } from "./use-shared-swr-poller";
import { useVerge } from "./use-verge";
export const useProxiesData = () => {
const { mutate: globalMutate } = useSWRConfig();
const { data, error, isLoading } = useSWR("getProxies", calcuProxies, {
...SWR_REALTIME,
refreshInterval: 0,
onError: (err) => console.warn("[AppData] Proxy fetch failed:", err),
});
const refreshProxy = useCallback(
() => globalMutate("getProxies"),
[globalMutate],
);
const pollerRefresh = useCallback(() => {
void globalMutate("getProxies");
}, [globalMutate]);
useSharedSWRPoller("getProxies", SWR_REALTIME.refreshInterval, pollerRefresh);
return {
proxies: data,
refreshProxy,
isLoading,
error,
};
};
export const useClashConfig = () => {
const { mutate: globalMutate } = useSWRConfig();
const { data, error, isLoading } = useSWR("getClashConfig", getBaseConfig, {
...SWR_SLOW_POLL,
refreshInterval: 0,
});
const refreshClashConfig = useCallback(
() => globalMutate("getClashConfig"),
[globalMutate],
);
const pollerRefresh = useCallback(() => {
void globalMutate("getClashConfig");
}, [globalMutate]);
useSharedSWRPoller(
"getClashConfig",
SWR_SLOW_POLL.refreshInterval,
pollerRefresh,
);
return {
clashConfig: data,
refreshClashConfig,
isLoading,
error,
};
};
export const useProxyProvidersData = () => {
const { data, error, isLoading, mutate } = useSWR(
"getProxyProviders",
calcuProxyProviders,
SWR_DEFAULTS,
);
const refreshProxyProviders = useCallback(() => mutate(), [mutate]);
return {
proxyProviders: data || {},
refreshProxyProviders,
isLoading,
error,
};
};
export const useRuleProvidersData = () => {
const { data, error, isLoading, mutate } = useSWR(
"getRuleProviders",
getRuleProviders,
SWR_DEFAULTS,
);
const refreshRuleProviders = useCallback(() => mutate(), [mutate]);
return {
ruleProviders: data?.providers || {},
refreshRuleProviders,
isLoading,
error,
};
};
export const useRulesData = () => {
const { data, error, isLoading, mutate } = useSWR(
"getRules",
getRules,
SWR_DEFAULTS,
);
const refreshRules = useCallback(() => mutate(), [mutate]);
return {
rules: data?.rules || [],
refreshRules,
isLoading,
error,
};
};
export const useSystemProxyData = () => {
const { data, error, isLoading, mutate } = useSWR(
"getSystemProxy",
getSystemProxy,
SWR_DEFAULTS,
);
const refreshSysproxy = useCallback(() => mutate(), [mutate]);
return {
sysproxy: data,
refreshSysproxy,
isLoading,
error,
};
};
type ClashConfig = Awaited<ReturnType<typeof getBaseConfig>>;
type SystemProxy = Awaited<ReturnType<typeof getSystemProxy>>;
interface SystemProxyAddressParams {
clashConfig?: ClashConfig | null;
sysproxy?: SystemProxy | null;
}
export const useSystemProxyAddress = ({
clashConfig,
sysproxy,
}: SystemProxyAddressParams) => {
const { verge } = useVerge();
return useMemo(() => {
if (!verge || !clashConfig) return "-";
const isPacMode = verge.proxy_auto_config ?? false;
if (isPacMode) {
const proxyHost = verge.proxy_host || "127.0.0.1";
const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897;
return [proxyHost, proxyPort].join(":");
}
const systemServer = sysproxy?.server;
if (systemServer && systemServer !== "-" && !systemServer.startsWith(":")) {
return systemServer;
}
const proxyHost = verge.proxy_host || "127.0.0.1";
const proxyPort = verge.verge_mixed_port || clashConfig.mixedPort || 7897;
return [proxyHost, proxyPort].join(":");
}, [clashConfig, sysproxy, verge]);
};
export const useAppUptime = () => {
const { data, error, isLoading } = useSWR("appUptime", getAppUptime, {
...SWR_DEFAULTS,
refreshInterval: 3000,
errorRetryCount: 1,
});
return {
uptime: data || 0,
error,
isLoading,
};
};
export const useRefreshAll = () => {
const { mutate } = useSWRConfig();
return useCallback(async () => {
await Promise.all([
mutate("getProxies"),
mutate("getClashConfig"),
mutate("getRules"),
mutate("getSystemProxy"),
mutate("getProxyProviders"),
mutate("getRuleProviders"),
]);
}, [mutate]);
};
-76
View File
@@ -1,76 +0,0 @@
import { useMemo } from "react";
import { useAppData } from "@/providers/app-data-context";
// 定义代理组类型
interface ProxyGroup {
name: string;
now: string;
}
// 获取当前代理节点信息的自定义Hook
export const useCurrentProxy = () => {
// 从AppDataProvider获取数据
const { proxies, clashConfig, refreshProxy } = useAppData();
// 获取当前模式
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
// 获取当前代理节点信息
const currentProxyInfo = useMemo(() => {
if (!proxies) return { currentProxy: null, primaryGroupName: null };
const { global, groups, records } = proxies;
// 默认信息
let primaryGroupName = "GLOBAL";
let currentName = global?.now;
// 在规则模式下,寻找主要代理组(通常是第一个或者名字包含特定关键词的组)
if (currentMode === "rule" && groups.length > 0) {
// 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组)
const primaryKeywords = [
"auto",
"select",
"proxy",
"节点选择",
"自动选择",
];
const primaryGroup =
groups.find((group: ProxyGroup) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0];
if (primaryGroup) {
primaryGroupName = primaryGroup.name;
currentName = primaryGroup.now;
}
}
// 如果找不到当前节点,返回null
if (!currentName) return { currentProxy: null, primaryGroupName };
// 获取完整的节点信息
const currentProxy = records[currentName] || {
name: currentName,
type: "Unknown",
udp: false,
xudp: false,
tfo: false,
mptcp: false,
smux: false,
history: [],
};
return { currentProxy, primaryGroupName };
}, [proxies, currentMode]);
return {
currentProxy: currentProxyInfo.currentProxy,
primaryGroupName: currentProxyInfo.primaryGroupName,
mode: currentMode,
refreshProxy,
};
};
+129
View File
@@ -0,0 +1,129 @@
import { useEffect } from "react";
import type { Key } from "swr";
type SharedPollerEntry = {
subscribers: number;
timer: number | null;
interval: number;
callback: (() => void) | null;
lastFired: number;
refreshWhenHidden: boolean;
refreshWhenOffline: boolean;
};
const sharedPollers = new Map<string, SharedPollerEntry>();
const isDocumentHidden = () => {
if (typeof document === "undefined") return false;
return document.visibilityState === "hidden";
};
const isOffline = () => {
if (typeof navigator === "undefined") return false;
return navigator.onLine === false;
};
const ensureTimer = (key: string, entry: SharedPollerEntry) => {
if (typeof window === "undefined") return;
if (entry.timer !== null) {
clearInterval(entry.timer);
}
entry.timer = window.setInterval(() => {
if (!entry.refreshWhenHidden && isDocumentHidden()) return;
if (!entry.refreshWhenOffline && isOffline()) return;
const now = Date.now();
if (entry.lastFired && now - entry.lastFired < entry.interval / 2) {
// Skip duplicate fire within half interval to coalesce concurrent consumers
return;
}
entry.lastFired = now;
entry.callback?.();
}, entry.interval);
};
const registerSharedPoller = (
key: string,
interval: number,
callback: () => void,
options: { refreshWhenHidden: boolean; refreshWhenOffline: boolean },
) => {
let entry = sharedPollers.get(key);
if (!entry) {
entry = {
subscribers: 0,
timer: null,
interval,
callback,
lastFired: 0,
refreshWhenHidden: options.refreshWhenHidden,
refreshWhenOffline: options.refreshWhenOffline,
};
sharedPollers.set(key, entry);
}
entry.subscribers += 1;
entry.callback = callback;
entry.interval = Math.min(entry.interval, interval);
entry.refreshWhenHidden =
entry.refreshWhenHidden || options.refreshWhenHidden;
entry.refreshWhenOffline =
entry.refreshWhenOffline || options.refreshWhenOffline;
ensureTimer(key, entry);
return () => {
const current = sharedPollers.get(key);
if (!current) return;
current.subscribers -= 1;
if (current.subscribers <= 0) {
if (current.timer !== null) {
clearInterval(current.timer);
}
sharedPollers.delete(key);
}
};
};
const normalizeKey = (key: Key): string | null => {
if (typeof key === "string") return key;
if (typeof key === "number" || typeof key === "boolean") return String(key);
if (Array.isArray(key)) {
try {
return JSON.stringify(key);
} catch {
return null;
}
}
return null;
};
export interface SharedSWRPollerOptions {
refreshWhenHidden?: boolean;
refreshWhenOffline?: boolean;
}
export const useSharedSWRPoller = (
key: Key,
interval?: number,
callback?: () => void,
options?: SharedSWRPollerOptions,
) => {
const refreshWhenHidden = options?.refreshWhenHidden ?? false;
const refreshWhenOffline = options?.refreshWhenOffline ?? false;
useEffect(() => {
if (!key || !interval || interval <= 0 || !callback) return;
const serializedKey = normalizeKey(key);
if (!serializedKey) return;
return registerSharedPoller(serializedKey, interval, callback, {
refreshWhenHidden,
refreshWhenOffline,
});
}, [key, interval, callback, refreshWhenHidden, refreshWhenOffline]);
};
+2 -2
View File
@@ -2,14 +2,14 @@ import { useLockFn } from "ahooks";
import useSWR, { mutate } from "swr";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useSystemProxyData } from "@/hooks/use-clash-data";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { getAutotemProxy } from "@/services/cmds";
// 系统代理状态检测统一逻辑
export const useSystemProxyState = () => {
const { verge, mutateVerge, patchVerge } = useVerge();
const { sysproxy } = useAppData();
const { sysproxy } = useSystemProxyData();
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, {
revalidateOnFocus: true,
revalidateOnReconnect: true,
+11 -1
View File
@@ -4,6 +4,7 @@ import useSWR from "swr";
import { getRunningMode, isAdmin, isServiceAvailable } from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import { useSharedSWRPoller } from "./use-shared-swr-poller";
import { useVerge } from "./use-verge";
export interface SystemState {
@@ -43,11 +44,20 @@ export function useSystemState() {
},
{
suspense: true,
refreshInterval: 30000,
refreshInterval: 0,
fallback: defaultSystemState,
},
);
useSharedSWRPoller(
"getSystemState",
30000,
() => {
void mutateSystemState();
},
{ refreshWhenHidden: false, refreshWhenOffline: false },
);
const isSidecarMode = systemState.runningMode === "Sidecar";
const isServiceMode = systemState.runningMode === "Service";
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk;
+6 -6
View File
@@ -121,14 +121,14 @@
"communication": "Core communication error"
},
"labels": {
"rule": "Rule",
"global": "Global",
"direct": "Direct"
"rule": "Режим правил",
"global": "Глобальный режим",
"direct": "Прямой режим"
},
"descriptions": {
"rule": "Автоматически выбирать прокси в зависимости от правил",
"global": "Перенаправлять все сетевые запросы через выбранный прокси",
"direct": "Подключение к интернету без прокси"
"rule": "Automatically choose proxies according to the rule set.",
"global": "Forward all network requests through the selected proxy.",
"direct": "Bypass the proxy and connect to the internet directly."
}
}
}
+9 -9
View File
@@ -73,17 +73,17 @@
"components": {
"enums": {
"strategies": {
"select": "select",
"url-test": "url-test",
"fallback": "fallback",
"load-balance": "load-balance",
"relay": "relay"
"select": "Выбор прокси вручную",
"url-test": "Выбор прокси на основе задержки теста URL",
"fallback": "Переключение на другой прокси при ошибке",
"load-balance": "Распределение прокси на основе балансировки нагрузки",
"relay": "Передача через определенную цепочку прокси"
},
"policies": {
"DIRECT": "DIRECT",
"REJECT": "REJECT",
"REJECT-DROP": "REJECT-DROP",
"PASS": "PASS"
"DIRECT": "Данные направляются напрямую наружу",
"REJECT": "Перехватывает запросы",
"REJECT-DROP": "Отклоняет запросы",
"PASS": "Пропускает это правило при совпадении"
}
}
}
+33 -33
View File
@@ -42,39 +42,39 @@
}
},
"ruleTypes": {
"DOMAIN": "DOMAIN",
"DOMAIN-SUFFIX": "DOMAIN-SUFFIX",
"DOMAIN-KEYWORD": "DOMAIN-KEYWORD",
"DOMAIN-REGEX": "DOMAIN-REGEX",
"GEOSITE": "GEOSITE",
"GEOIP": "GEOIP",
"SRC-GEOIP": "SRC-GEOIP",
"IP-ASN": "IP-ASN",
"SRC-IP-ASN": "SRC-IP-ASN",
"IP-CIDR": "IP-CIDR",
"IP-CIDR6": "IP-CIDR6",
"SRC-IP-CIDR": "SRC-IP-CIDR",
"IP-SUFFIX": "IP-SUFFIX",
"SRC-IP-SUFFIX": "SRC-IP-SUFFIX",
"SRC-PORT": "SRC-PORT",
"DST-PORT": "DST-PORT",
"IN-PORT": "IN-PORT",
"DSCP": "DSCP",
"PROCESS-NAME": "PROCESS-NAME",
"PROCESS-PATH": "PROCESS-PATH",
"PROCESS-NAME-REGEX": "PROCESS-NAME-REGEX",
"PROCESS-PATH-REGEX": "PROCESS-PATH-REGEX",
"NETWORK": "NETWORK",
"UID": "UID",
"IN-TYPE": "IN-TYPE",
"IN-USER": "IN-USER",
"IN-NAME": "IN-NAME",
"SUB-RULE": "SUB-RULE",
"RULE-SET": "RULE-SET",
"AND": "AND",
"OR": "OR",
"NOT": "NOT",
"MATCH": "MATCH"
"DOMAIN": "Соответствует полному доменному имени",
"DOMAIN-SUFFIX": "Соответствует суффиксу домена",
"DOMAIN-KEYWORD": "Соответствует ключевому слову домена",
"DOMAIN-REGEX": "Соответствует домену с использованием регулярных выражений",
"GEOSITE": "Соответствует доменам в Geosite",
"GEOIP": "Соответствует коду страны IP-адреса",
"SRC-GEOIP": "Соответствует коду страны исходного IP-адреса",
"IP-ASN": "Соответствует ASN IP-адреса",
"SRC-IP-ASN": "Соответствует ASN исходного IP-адреса",
"IP-CIDR": "Соответствует диапазону IP-адресов",
"IP-CIDR6": "Соответствует диапазону IPv6-адресов",
"SRC-IP-CIDR": "Соответствует диапазону исходных IP-адресов",
"IP-SUFFIX": "Соответствует диапазону суффиксов IP-адресов",
"SRC-IP-SUFFIX": "Соответствует диапазону суффиксов исходных IP-адресов",
"SRC-PORT": "Соответствует диапазону исходных портов",
"DST-PORT": "Соответствует диапазону целевых портов",
"IN-PORT": "Соответствует входящему порту",
"DSCP": "Маркировка DSCP (только для tproxy UDP входящего)",
"PROCESS-NAME": "Соответствует имени процесса (имя пакета Android)",
"PROCESS-PATH": "Соответствует полному пути процесса",
"PROCESS-NAME-REGEX": "Соответствует имени процесса с использованием регулярных выражений (имя пакета Android)",
"PROCESS-PATH-REGEX": "Соответствует полному пути процесса с использованием регулярных выражений",
"NETWORK": "Соответствует транспортному протоколу (tcp/udp)",
"UID": "Соответствует USER ID в Linux",
"IN-TYPE": "Соответствует типу входящего соединения",
"IN-USER": "Соответствует имени пользователя входящего соединения",
"IN-NAME": "Соответствует имени входящего соединения",
"SUB-RULE": "Подправило",
"RULE-SET": "Соответствует набору правил",
"AND": "Логическое И",
"OR": "Логическое ИЛИ",
"NOT": "Логическое НЕ",
"MATCH": "Соответствует всем запросам"
},
"title": "Редактировать правила"
}
+2 -14
View File
@@ -23,8 +23,8 @@ import {
} from "@mui/material";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import type { CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useNavigate } from "react-router";
import { SWRConfig } from "swr";
@@ -257,24 +257,12 @@ const Layout = () => {
<SWRConfig
value={{
errorRetryCount: 3,
// TODO remove the 5000ms
errorRetryInterval: 5000,
onError: (error, key) => {
// FIXME the condition should not be handle gllobally
console.error(`[SWR Error] Key: ${key}, Error:`, error);
if (key !== "getAutotemProxy") {
console.error(`SWR Error for ${key}:`, error);
return;
}
// FIXME we need a better way to handle the retry when first booting app
const silentKeys = [
"getVersion",
"getClashConfig",
"getAutotemProxy",
];
if (silentKeys.includes(key)) return;
console.error(`[SWR Error] Key: ${key}, Error:`, error);
},
dedupingInterval: 2000,
}}
+8 -3
View File
@@ -7,12 +7,13 @@ import { BaseEmpty, BasePage, BaseSearchBox } from "@/components/base";
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
import { ProviderButton } from "@/components/rule/provider-button";
import RuleItem from "@/components/rule/rule-item";
import { useRuleProvidersData, useRulesData } from "@/hooks/use-clash-data";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-context";
const RulesPage = () => {
const { t } = useTranslation();
const { rules = [], refreshRules, refreshRuleProviders } = useAppData();
const { rules = [], refreshRules } = useRulesData();
const { ruleProviders, refreshRuleProviders } = useRuleProvidersData();
const [match, setMatch] = useState(() => (_: string) => true);
const virtuosoRef = useRef<VirtuosoHandle>(null);
const [showScrollTop, setShowScrollTop] = useState(false);
@@ -56,7 +57,11 @@ const RulesPage = () => {
}}
header={
<Box display="flex" alignItems="center" gap={1}>
<ProviderButton />
<ProviderButton
ruleProviders={ruleProviders}
refreshRuleProviders={refreshRuleProviders}
refreshRules={refreshRules}
/>
</Box>
}
>
-51
View File
@@ -1,51 +0,0 @@
import { createContext, use } from "react";
import {
BaseConfig,
ProxyProvider,
Rule,
RuleProvider,
} from "tauri-plugin-mihomo-api";
export interface AppDataContextType {
proxies: any;
clashConfig: BaseConfig;
rules: Rule[];
sysproxy: any;
runningMode?: string;
uptime: number;
proxyProviders: Record<string, ProxyProvider>;
ruleProviders: Record<string, RuleProvider>;
systemProxyAddress: string;
refreshProxy: () => Promise<any>;
refreshClashConfig: () => Promise<any>;
refreshRules: () => Promise<any>;
refreshSysproxy: () => Promise<any>;
refreshProxyProviders: () => Promise<any>;
refreshRuleProviders: () => Promise<any>;
refreshAll: () => Promise<any>;
}
export interface ConnectionWithSpeed extends IConnectionsItem {
curUpload: number;
curDownload: number;
}
export interface ConnectionSpeedData {
id: string;
upload: number;
download: number;
timestamp: number;
}
export const AppDataContext = createContext<AppDataContextType | null>(null);
export const useAppData = () => {
const context = use(AppDataContext);
if (!context) {
throw new Error("useAppData必须在AppDataProvider内使用");
}
return context;
};
+18 -175
View File
@@ -1,67 +1,25 @@
import { listen } from "@tauri-apps/api/event";
import React, { useCallback, useEffect, useMemo } from "react";
import useSWR from "swr";
import {
getBaseConfig,
getRuleProviders,
getRules,
} from "tauri-plugin-mihomo-api";
import { PropsWithChildren, useCallback, useEffect } from "react";
import { useSWRConfig } from "swr";
import { useVerge } from "@/hooks/use-verge";
import {
calcuProxies,
calcuProxyProviders,
getAppUptime,
getRunningMode,
getSystemProxy,
} from "@/services/cmds";
import { SWR_DEFAULTS, SWR_REALTIME, SWR_SLOW_POLL } from "@/services/config";
// 负责监听全局事件并驱动 SWR 刷新,避免包裹全局 context 带来的额外渲染
export const AppDataProvider = ({ children }: PropsWithChildren) => {
useAppDataEventBridge();
return <>{children}</>;
};
import { AppDataContext, AppDataContextType } from "./app-data-context";
const useAppDataEventBridge = () => {
const { mutate } = useSWRConfig();
// 全局数据提供者组件
export const AppDataProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const { verge } = useVerge();
const { data: proxiesData, mutate: refreshProxy } = useSWR(
"getProxies",
calcuProxies,
{
...SWR_REALTIME,
onError: (_) => {
// FIXME when we intially start the app, and core is starting,
// there will be error thrown by getProxies API.
// We should handle this case properly later.
},
},
const refreshProxy = useCallback(() => mutate("getProxies"), [mutate]);
const refreshClashConfig = useCallback(
() => mutate("getClashConfig"),
[mutate],
);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getBaseConfig,
SWR_SLOW_POLL,
);
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders",
calcuProxyProviders,
SWR_DEFAULTS,
);
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
"getRuleProviders",
getRuleProviders,
SWR_DEFAULTS,
);
const { data: rulesData, mutate: refreshRules } = useSWR(
"getRules",
getRules,
SWR_DEFAULTS,
const refreshRules = useCallback(() => mutate("getRules"), [mutate]);
const refreshRuleProviders = useCallback(
() => mutate("getRuleProviders"),
[mutate],
);
useEffect(() => {
@@ -224,125 +182,10 @@ export const AppDataProvider = ({
if (errors.length > 0) {
console.error(
`[DataProvider] ${errors.length} errors during cleanup:`,
"[DataProvider] " + errors.length + " errors during cleanup:",
errors,
);
}
};
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders]);
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
"getSystemProxy",
getSystemProxy,
SWR_DEFAULTS,
);
const { data: runningMode } = useSWR(
"getRunningMode",
getRunningMode,
SWR_DEFAULTS,
);
const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
...SWR_DEFAULTS,
refreshInterval: 3000,
errorRetryCount: 1,
});
// 提供统一的刷新方法
const refreshAll = useCallback(async () => {
await Promise.all([
refreshProxy(),
refreshClashConfig(),
refreshRules(),
refreshSysproxy(),
refreshProxyProviders(),
refreshRuleProviders(),
]);
}, [
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
]);
// 聚合所有数据
const value = useMemo(() => {
// 计算系统代理地址
const calculateSystemProxyAddress = () => {
if (!verge || !clashConfig) return "-";
const isPacMode = verge.proxy_auto_config ?? false;
if (isPacMode) {
// PAC模式:显示我们期望设置的代理地址
const proxyHost = verge.proxy_host || "127.0.0.1";
const proxyPort =
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
return `${proxyHost}:${proxyPort}`;
} else {
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
const systemServer = sysproxy?.server;
if (
systemServer &&
systemServer !== "-" &&
!systemServer.startsWith(":")
) {
return systemServer;
} else {
// 系统地址无效,返回期望的代理地址
const proxyHost = verge.proxy_host || "127.0.0.1";
const proxyPort =
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
return `${proxyHost}:${proxyPort}`;
}
}
};
return {
// 数据
proxies: proxiesData,
clashConfig,
rules: rulesData?.rules || [],
sysproxy,
runningMode,
uptime: uptimeData || 0,
// 提供者数据
proxyProviders: proxyProviders || {},
ruleProviders: ruleProviders?.providers || {},
systemProxyAddress: calculateSystemProxyAddress(),
// 刷新方法
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
refreshAll,
} as AppDataContextType;
}, [
proxiesData,
clashConfig,
rulesData,
sysproxy,
runningMode,
uptimeData,
proxyProviders,
ruleProviders,
verge,
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
refreshAll,
]);
return <AppDataContext value={value}>{children}</AppDataContext>;
};