From 093b8d4c3453b8d4438a937424eaebdfed807995 Mon Sep 17 00:00:00 2001 From: DHR60 <192860629+DHR60@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:21:46 +0000 Subject: [PATCH] Adjust Clash API (#10088) --- .../Base/BulkObservableCollection.cs | 34 +- v2rayN/ServiceLib/Enums/ERuleMode.cs | 1 - v2rayN/ServiceLib/GlobalUsings.cs | 3 +- v2rayN/ServiceLib/Manager/ClashApiManager.cs | 220 ++++----- .../ServiceLib/Models/Configs/ConfigItems.cs | 1 - v2rayN/ServiceLib/Models/Dto/ClashItem.cs | 44 ++ .../ServiceLib/Models/Dto/ClashProviders.cs | 16 - v2rayN/ServiceLib/Models/Dto/ClashProxies.cs | 23 - .../ServiceLib/Models/Dto/ClashProxyModel.cs | 4 +- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 18 +- v2rayN/ServiceLib/Resx/ResUI.fa.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.fr.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.hu.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.id.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 2 +- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 2 +- .../CoreConfig/CoreConfigClashService.cs | 14 +- .../ViewModels/ClashConnectionsViewModel.cs | 101 ++--- .../ViewModels/ClashProxiesViewModel.cs | 418 +++++++----------- .../Views/ClashProxiesView.axaml | 14 +- .../Views/ClashProxiesView.axaml.cs | 7 +- v2rayN/v2rayN/Views/ClashProxiesView.xaml | 14 +- v2rayN/v2rayN/Views/ClashProxiesView.xaml.cs | 7 +- 25 files changed, 440 insertions(+), 515 deletions(-) create mode 100644 v2rayN/ServiceLib/Models/Dto/ClashItem.cs delete mode 100644 v2rayN/ServiceLib/Models/Dto/ClashProviders.cs delete mode 100644 v2rayN/ServiceLib/Models/Dto/ClashProxies.cs diff --git a/v2rayN/ServiceLib/Base/BulkObservableCollection.cs b/v2rayN/ServiceLib/Base/BulkObservableCollection.cs index 37e026cd..de8b33ae 100644 --- a/v2rayN/ServiceLib/Base/BulkObservableCollection.cs +++ b/v2rayN/ServiceLib/Base/BulkObservableCollection.cs @@ -2,7 +2,14 @@ namespace ServiceLib.Base; public class BulkObservableCollection : ObservableCollection { - private bool _suppressNotification = false; + private bool _suppressNotification; + + public BulkObservableCollection() + { + } + + public BulkObservableCollection(IEnumerable collection) : base(collection) { } + public BulkObservableCollection(List list) : base(list) { } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { @@ -63,4 +70,29 @@ public class BulkObservableCollection : ObservableCollection return true; } + + public void ReplaceRange(IEnumerable? collection) + { + if (collection == null) + { + return; + } + + _suppressNotification = true; + try + { + Items.Clear(); + foreach (var item in collection) + { + Items.Add(item); + } + } + finally + { + _suppressNotification = false; + OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); + OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } } diff --git a/v2rayN/ServiceLib/Enums/ERuleMode.cs b/v2rayN/ServiceLib/Enums/ERuleMode.cs index fd89449d..5f8a8028 100644 --- a/v2rayN/ServiceLib/Enums/ERuleMode.cs +++ b/v2rayN/ServiceLib/Enums/ERuleMode.cs @@ -5,5 +5,4 @@ public enum ERuleMode Rule = 0, Global = 1, Direct = 2, - Unchanged = 3 } diff --git a/v2rayN/ServiceLib/GlobalUsings.cs b/v2rayN/ServiceLib/GlobalUsings.cs index 11b75497..b7b3ecc4 100644 --- a/v2rayN/ServiceLib/GlobalUsings.cs +++ b/v2rayN/ServiceLib/GlobalUsings.cs @@ -8,6 +8,7 @@ global using System.Net.NetworkInformation; global using System.Net.Sockets; global using ReactiveUI.Primitives; global using ReactiveUI.Primitives.Concurrency; +global using ReactiveUI.Primitives.Extensions; global using ReactiveUI.Primitives.Disposables; global using ReactiveUI.Primitives.Signals; global using System.Reflection; @@ -41,5 +42,3 @@ global using ServiceLib.Services; global using ServiceLib.Services.CoreConfig; global using ServiceLib.Services.Statistics; global using SQLite; - - diff --git a/v2rayN/ServiceLib/Manager/ClashApiManager.cs b/v2rayN/ServiceLib/Manager/ClashApiManager.cs index 05232e5a..1862efd9 100644 --- a/v2rayN/ServiceLib/Manager/ClashApiManager.cs +++ b/v2rayN/ServiceLib/Manager/ClashApiManager.cs @@ -1,113 +1,104 @@ -using static ServiceLib.Models.Dto.ClashProxies; - namespace ServiceLib.Manager; public sealed class ClashApiManager { + private const string _tag = "ClashApiManager"; private static readonly Lazy instance = new(() => new()); public static ClashApiManager Instance => instance.Value; - private static readonly string _tag = "ClashApiHandler"; - private Dictionary? _proxies; - public Dictionary ProfileContent { get; set; } + private static string ApiUrl => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}"; - public async Task?> GetClashProxiesAsync() + public async Task GetProxies() { for (var i = 0; i < 3; i++) { - var url = $"{GetApiUrl()}/proxies"; - var result = await HttpClientHelper.Instance.TryGetAsync(url); - var clashProxies = JsonUtils.Deserialize(result); + var url = $"{ApiUrl}/proxies"; + var resultTask = HttpClientHelper.Instance.TryGetAsync(url); - var url2 = $"{GetApiUrl()}/providers/proxies"; - var result2 = await HttpClientHelper.Instance.TryGetAsync(url2); + var url2 = $"{ApiUrl}/providers/proxies"; + var result2Task = HttpClientHelper.Instance.TryGetAsync(url2); + + await Task.WhenAll(resultTask, result2Task); + + var result = await resultTask; + var result2 = await result2Task; + var clashProxies = JsonUtils.Deserialize(result); var clashProviders = JsonUtils.Deserialize(result2); - if (clashProxies != null || clashProviders != null) + if (clashProxies is null && clashProviders is null) { - _proxies = clashProxies?.proxies; - return new Tuple(clashProxies, clashProviders); + await Task.Delay(2000); + continue; } - await Task.Delay(2000); + var item = new ClashItem(); + if (clashProxies?.proxies is { Count: > 0 } proxies) + { + item = item with { Proxies = proxies }; + } + if (clashProviders.providers is { Count: > 0 } providers) + { + foreach (var provider in providers) + { + foreach (var proxy in provider.Value.proxies ?? []) + { + if (string.IsNullOrEmpty(proxy.name) + || !item.Proxies.TryAdd(proxy.name, proxy)) + { + continue; + } + item.ProviderIndexMap.Add(proxy.name, provider.Key); + } + } + } + + return item; } return null; } - public void ClashProxiesDelayTest(bool blAll, List lstProxy, Func updateFunc) + public async Task TestDelay(string name, ClashItem? clashItem) { - Task.Run(async () => + if (clashItem?.ProviderIndexMap.TryGetValue(name, out var providerName) == true) { - if (blAll) - { - if (_proxies == null) - { - await GetClashProxiesAsync(); - } - lstProxy = []; - lstProxy.AddRange(from kv in _proxies ?? [] - where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower()) - select new ClashProxyModel() - { - Name = kv.Value.name, - Type = kv.Value.type?.ToLower(), - }); - } - - if (lstProxy is not { Count: > 0 }) - { - return; - } - var urlBase = $"{GetApiUrl()}/proxies"; - urlBase += @"/{0}/delay?timeout=10000&url=" + AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; - - var tasks = new List(); - foreach (var it in lstProxy) - { - if (Global.notAllowTestType.Contains(it.Type.ToLower())) - { - continue; - } - var name = it.Name; - var url = string.Format(urlBase, name); - tasks.Add(Task.Run(async () => - { - var result = await HttpClientHelper.Instance.TryGetAsync(url); - await updateFunc?.Invoke(it, result); - })); - } - await Task.WhenAll(tasks); - await Task.Delay(1000); - await updateFunc?.Invoke(null, ""); - }); + return await TestProviderProxyDelay(name, providerName); + } + return await TestProxyDelay(name); } - public List? GetClashProxyGroups() + private async Task TestProxyDelay(string name) + { + var url = $"{ApiUrl}/proxies/{Utils.UrlEncode(name)}/delay?timeout=10000&url=" + + Utils.UrlEncode(AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl); + return await SendTestRequest(url); + } + + private async Task TestProviderProxyDelay(string name, string providerName) + { + var url = + $"{ApiUrl}/providers/proxies/{Utils.UrlEncode(providerName)}/{Utils.UrlEncode(name)}/healthcheck?timeout=10000&url=" + + Utils.UrlEncode(AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl); + return await SendTestRequest(url); + } + + private async Task SendTestRequest(string url) + { + var result = await HttpClientHelper.Instance.TryGetAsync(url); + var jsonObject = JsonUtils.ParseJson(result) as JsonObject; + return jsonObject?["delay"] is { } n && n.GetValueKind() == JsonValueKind.Number && + n.GetValue().TryGetInt32(out var v) + ? v + : -1; + } + + public async Task SetActiveProxy(string groupName, string nodeName) { try { - var fileContent = ProfileContent; - if (fileContent is null || fileContent?.ContainsKey("proxy-groups") == false) - { - return null; - } - return JsonUtils.Deserialize>(JsonUtils.Serialize(fileContent["proxy-groups"])); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - return null; - } - } - - public async Task ClashSetActiveProxy(string name, string nameNode) - { - try - { - var url = $"{GetApiUrl()}/proxies/{name}"; + var url = $"{ApiUrl}/proxies/{Utils.UrlEncode(groupName)}"; var headers = new Dictionary(); - headers.Add("name", nameNode); + headers.Add("name", nodeName); await HttpClientHelper.Instance.PutAsync(url, headers); } catch (Exception ex) @@ -116,39 +107,67 @@ public sealed class ClashApiManager } } - public async Task ClashConfigUpdate(Dictionary headers) + public async Task UpdateClashMode(string mode) { - if (_proxies == null) + var headers = new Dictionary { - return; - } + { "mode", mode }, + }; + await UpdateConfig(headers); + } - var urlBase = $"{GetApiUrl()}/configs"; + public async Task UpdateConfig(Dictionary headers) + { + var urlBase = $"{ApiUrl}/configs"; await HttpClientHelper.Instance.PatchAsync(urlBase, headers); } - public async Task ClashConfigReload(string filePath) + public async Task> GetClashModes() { - await ClashConnectionClose(""); - try + var jsonNode = await GetConfig(); + if ((jsonNode?["mode-list"] ?? jsonNode?["modes"]) is not JsonArray { Count: > 0 } modesArray) { - var url = $"{GetApiUrl()}/configs?force=true"; - var headers = new Dictionary(); - headers.Add("path", filePath); - await HttpClientHelper.Instance.PutAsync(url, headers); + return []; } - catch (Exception ex) + var modes = new List(); + foreach (var mode in modesArray) { - Logging.SaveLog(_tag, ex); + if (mode is JsonValue jsonValue && jsonValue.GetValueKind() == JsonValueKind.String) + { + modes.Add(jsonValue.GetValue()); + } } + return modes; } - public async Task GetClashConnectionsAsync() + public async Task GetClashMode() + { + var jsonNode = await GetConfig(); + if (jsonNode["mode"] is not JsonValue jsonValue || jsonValue.GetValueKind() != JsonValueKind.String) + { + return null; + } + return jsonValue.GetValue(); + } + + public async Task GetConfig() + { + var url = $"{ApiUrl}/configs"; + var result = await HttpClientHelper.Instance.TryGetAsync(url); + var jsonNode = JsonUtils.ParseJson(result); + if (jsonNode is not JsonObject jsonObject) + { + return new JsonObject(); + } + return jsonObject; + } + + public async Task GetConnections() { try { - var url = $"{GetApiUrl()}/connections"; + var url = $"{ApiUrl}/connections"; var result = await HttpClientHelper.Instance.TryGetAsync(url); var clashConnections = JsonUtils.Deserialize(result); @@ -162,11 +181,11 @@ public sealed class ClashApiManager return null; } - public async Task ClashConnectionClose(string id) + public async Task CloseConnection(string id) { try { - var url = $"{GetApiUrl()}/connections/{id}"; + var url = $"{ApiUrl}/connections/{id}"; await HttpClientHelper.Instance.DeleteAsync(url); } catch (Exception ex) @@ -174,9 +193,4 @@ public sealed class ClashApiManager Logging.SaveLog(_tag, ex); } } - - private string GetApiUrl() - { - return $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}"; - } } diff --git a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index 11b45000..e912ed26 100644 --- a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs @@ -209,7 +209,6 @@ public class HysteriaItem [Serializable] public class ClashUIItem { - public ERuleMode RuleMode { get; set; } public bool EnableIPv6 { get; set; } public bool EnableMixinContent { get; set; } public int ProxiesSorting { get; set; } diff --git a/v2rayN/ServiceLib/Models/Dto/ClashItem.cs b/v2rayN/ServiceLib/Models/Dto/ClashItem.cs new file mode 100644 index 00000000..1495858f --- /dev/null +++ b/v2rayN/ServiceLib/Models/Dto/ClashItem.cs @@ -0,0 +1,44 @@ +namespace ServiceLib.Models.Dto; + +public record ClashItem +{ + public Dictionary Proxies { get; init; } = []; + public Dictionary ProviderIndexMap { get; init; } = []; + + public bool IsEmpty() => Proxies.Count == 0; +} + +public record ClashProxy +{ + public List? all { get; init; } + public List? history { get; init; } + public string? name { get; init; } + public string? type { get; init; } + public bool udp { get; init; } + public string? now { get; init; } + public int delay { get; init; } + + public record HistoryItem + { + public string? time { get; init; } + public int delay { get; init; } + } +} + +public record ClashProvider +{ + public string? name { get; init; } + public List? proxies { get; init; } + public string? type { get; init; } + public string? vehicleType { get; init; } +} + +public record ClashProxies +{ + public Dictionary proxies { get; init; } = []; +} + +public record ClashProviders +{ + public Dictionary providers { get; init; } = []; +} diff --git a/v2rayN/ServiceLib/Models/Dto/ClashProviders.cs b/v2rayN/ServiceLib/Models/Dto/ClashProviders.cs deleted file mode 100644 index 83040906..00000000 --- a/v2rayN/ServiceLib/Models/Dto/ClashProviders.cs +++ /dev/null @@ -1,16 +0,0 @@ -using static ServiceLib.Models.Dto.ClashProxies; - -namespace ServiceLib.Models.Dto; - -public class ClashProviders -{ - public Dictionary? providers { get; set; } - - public class ProvidersItem - { - public string? name { get; set; } - public List? proxies { get; set; } - public string? type { get; set; } - public string? vehicleType { get; set; } - } -} diff --git a/v2rayN/ServiceLib/Models/Dto/ClashProxies.cs b/v2rayN/ServiceLib/Models/Dto/ClashProxies.cs deleted file mode 100644 index 94986e50..00000000 --- a/v2rayN/ServiceLib/Models/Dto/ClashProxies.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ServiceLib.Models.Dto; - -public class ClashProxies -{ - public Dictionary? proxies { get; set; } - - public class ProxiesItem - { - public List? all { get; set; } - public List? history { get; set; } - public string? name { get; set; } - public string? type { get; set; } - public bool udp { get; set; } - public string? now { get; set; } - public int delay { get; set; } - } - - public class HistoryItem - { - public string? time { get; set; } - public int delay { get; set; } - } -} diff --git a/v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs b/v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs index 535040d3..80469760 100644 --- a/v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs @@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto; [Serializable] public partial class ClashProxyModel : ReactiveObject { - public string? Name { get; set; } + public required string Name { get; set; } - public string? Type { get; set; } + public required string Type { get; set; } public string? Now { get; set; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index f7ceb75a..a37bd801 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -987,6 +987,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Latency Test 的本地化字符串。 + /// + public static string menuDelaytest { + get { + return ResourceManager.GetString("menuDelaytest", resourceCulture); + } + } + /// /// 查找类似 DNS Settings 的本地化字符串。 /// @@ -1401,15 +1410,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Latency Test 的本地化字符串。 - /// - public static string menuProxiesDelaytest { - get { - return ResourceManager.GetString("menuProxiesDelaytest", resourceCulture); - } - } - /// /// 查找类似 Part Node Latency Test 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa.resx b/v2rayN/ServiceLib/Resx/ResUI.fa.resx index fc2e3d91..e5662028 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa.resx @@ -1215,7 +1215,7 @@ قانون - + تست تأخیر diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index b86d96b0..06517fa1 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1212,7 +1212,7 @@ Règle - + Test de latence diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index a3e4367e..bdfb1c32 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1215,7 +1215,7 @@ Szabály - + Késleltetés teszt diff --git a/v2rayN/ServiceLib/Resx/ResUI.id.resx b/v2rayN/ServiceLib/Resx/ResUI.id.resx index 38e1ba5c..a4d44764 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.id.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.id.resx @@ -1215,7 +1215,7 @@ Aturan - + Uji latensi diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index a92c9bc4..58ca3903 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1221,7 +1221,7 @@ Rule - + Latency Test diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index fc743919..cb089813 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1221,7 +1221,7 @@ Правила - + Тест задержки diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index da3f5770..029cdc76 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1218,7 +1218,7 @@ 规则 - + 延迟测试 diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 6d84fa03..6ba6c355 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1218,7 +1218,7 @@ 規則 - + 延遲測試 diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs index 4c3b91aa..a8103e99 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs @@ -94,17 +94,7 @@ public class CoreConfigClashService(Config config, bool isTunEnabled) fileContent["ipv6"] = config.ClashUIItem.EnableIPv6; //mode - if (!fileContent.ContainsKey("mode")) - { - fileContent["mode"] = nameof(ERuleMode.Rule).ToLower(); - } - else - { - if (config.ClashUIItem.RuleMode != ERuleMode.Unchanged) - { - fileContent["mode"] = config.ClashUIItem.RuleMode.ToString().ToLower(); - } - } + fileContent.TryAdd("mode", nameof(ERuleMode.Rule)); //enable tun mode if (isTunEnabled) @@ -159,8 +149,6 @@ public class CoreConfigClashService(Config config, bool isTunEnabled) return ret; } - ClashApiManager.Instance.ProfileContent = fileContent; - ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}"); ret.Success = true; return ret; diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs index 053f000a..749565dd 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -2,33 +2,18 @@ namespace ServiceLib.ViewModels; public partial class ClashConnectionsViewModel : MyReactiveObject { - public BulkObservableCollection ConnectionItems { get; } = []; - - [Reactive] - public partial ClashConnectionModel SelectedSource { get; set; } - - public ReactiveCommand ConnectionCloseCmd { get; } - public ReactiveCommand ConnectionCloseAllCmd { get; } - - [Reactive] - public partial string HostFilter { get; set; } - - [Reactive] - public partial bool AutoRefresh { get; set; } - public ClashConnectionsViewModel() { _config = AppManager.Instance.Config; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; var canEditRemove = this.WhenAnyValue( - x => x.SelectedSource, - selectedSource => selectedSource != null && selectedSource.Id.IsNotEmpty()); + x => x.SelectedSource, + selectedSource => selectedSource?.Id?.IsNotEmpty() == true); this.WhenAnyValue( - x => x.AutoRefresh, - y => y == true) - .Subscribe(c => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; }); + x => x.AutoRefresh) + .Subscribe(_ => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; }); ConnectionCloseCmd = ReactiveCommand.CreateFromTask(async () => { await ClashConnectionClose(false); @@ -39,17 +24,28 @@ public partial class ClashConnectionsViewModel : MyReactiveObject await ClashConnectionClose(true); }); - _ = Init(); + _ = Task.Factory.StartNew( + async () => await GetClashConnectionsTask(), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default + ); } - private async Task Init() - { - await DelayTestTask(); - } + public BulkObservableCollection ConnectionItems { get; } = []; + + [Reactive] public partial ClashConnectionModel SelectedSource { get; set; } + + public ReactiveCommand ConnectionCloseCmd { get; } + public ReactiveCommand ConnectionCloseAllCmd { get; } + + [Reactive] public partial string HostFilter { get; set; } + + [Reactive] public partial bool AutoRefresh { get; set; } private async Task GetClashConnections() { - var ret = await ClashApiManager.Instance.GetClashConnectionsAsync(); + var ret = await ClashApiManager.Instance.GetConnections(); if (ret == null) { return; @@ -69,7 +65,8 @@ public partial class ClashConnectionsViewModel : MyReactiveObject var lstModel = new List(); foreach (var item in connections ?? []) { - var host = $"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}"; + var host = + $"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}"; if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter)) { continue; @@ -83,7 +80,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject Host = host, Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds, Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"), - Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}" + Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}", }; lstModel.Add(model); @@ -103,47 +100,39 @@ public partial class ClashConnectionsViewModel : MyReactiveObject if (!all) { var item = SelectedSource; - if (item is null) + if (string.IsNullOrEmpty(item?.Id)) { return; } id = item.Id; } - else - { - ConnectionItems.Clear(); - } - await ClashApiManager.Instance.ClashConnectionClose(id); + await ClashApiManager.Instance.CloseConnection(id); await GetClashConnections(); } - public async Task DelayTestTask() + public async Task GetClashConnectionsTask() { - _ = Task.Run(async () => + var numOfExecuted = 1; + while (true) { - var numOfExecuted = 1; - while (true) + await Task.Delay(1000 * 5); + numOfExecuted++; + if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && + AppManager.Instance.IsRunningCore(ECoreType.sing_box))) { - await Task.Delay(1000 * 5); - numOfExecuted++; - if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && AppManager.Instance.IsRunningCore(ECoreType.sing_box))) - { - continue; - } - - if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0) - { - continue; - } - - if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0) - { - continue; - } - await GetClashConnections(); + continue; } - }); - await Task.CompletedTask; + if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0) + { + continue; + } + + if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0) + { + continue; + } + await GetClashConnections(); + } } } diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index e2716dd8..cc76214b 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -1,36 +1,9 @@ -using static ServiceLib.Models.Dto.ClashProviders; -using static ServiceLib.Models.Dto.ClashProxies; - namespace ServiceLib.ViewModels; public partial class ClashProxiesViewModel : MyReactiveObject { - private Dictionary? _proxies; - private Dictionary? _providers; private readonly int _delayTimeout = 99999999; - - public BulkObservableCollection ProxyGroups { get; } = []; - public BulkObservableCollection ProxyDetails { get; } = []; - - [Reactive] - public partial ClashProxyModel SelectedGroup { get; set; } - - [Reactive] - public partial ClashProxyModel SelectedDetail { get; set; } - - public ReactiveCommand ProxiesReloadCmd { get; } - public ReactiveCommand ProxiesDelayTestCmd { get; } - public ReactiveCommand ProxiesDelayTestPartCmd { get; } - public ReactiveCommand ProxiesSelectActivityCmd { get; } - - [Reactive] - public partial int RuleModeSelected { get; set; } - - [Reactive] - public partial int SortingSelected { get; set; } - - [Reactive] - public partial bool AutoRefresh { get; set; } + private ClashItem _clashItem = new(); public ClashProxiesViewModel() { @@ -40,173 +13,158 @@ public partial class ClashProxiesViewModel : MyReactiveObject { await ProxiesReload(); }); - ProxiesDelayTestCmd = ReactiveCommand.CreateFromTask(async () => + ProxyDelayTestCmd = ReactiveCommand.CreateFromTask(async () => { - await ProxiesDelayTest(true); + if (!string.IsNullOrEmpty(SelectedDetail?.Name)) + { + await TestProxyDelay(SelectedDetail.Name); + } }); - ProxiesDelayTestPartCmd = ReactiveCommand.CreateFromTask(async () => + GroupProxiesDelayTestCmd = ReactiveCommand.CreateFromTask(async () => { - await ProxiesDelayTest(false); + await TestGroupProxiesDelay(); }); ProxiesSelectActivityCmd = ReactiveCommand.CreateFromTask(async () => { await SetActiveProxy(); }); - SelectedGroup = new(); - SelectedDetail = new(); AutoRefresh = _config.ClashUIItem.ProxiesAutoRefresh; SortingSelected = _config.ClashUIItem.ProxiesSorting; - RuleModeSelected = (int)_config.ClashUIItem.RuleMode; + RuleModeSelected = nameof(ERuleMode.Rule); #region WhenAnyValue && ReactiveCommand - this.WhenAnyValue( - x => x.SelectedGroup, - y => y != null && y.Name.IsNotEmpty()) - .Subscribe(RefreshProxyDetails); + this.WhenAnyValue(x => x.SelectedGroup) + .Where(y => y != null && y.Name.IsNotEmpty()) + .Subscribe(_ => RefreshProxyDetails()); - this.WhenAnyValue( - x => x.RuleModeSelected, - y => y >= 0) - .Subscribe(async c => await DoRuleModeSelected(c)); + this.WhenAnyValue(x => x.RuleModeSelected) + .Where(y => !string.IsNullOrEmpty(y)) + .Skip(1) + .SubscribeAsync(async x => await SetRuleMode(x)); - this.WhenAnyValue( - x => x.SortingSelected, - y => y >= 0) - .Subscribe(DoSortingSelected); + this.WhenAnyValue(x => x.SortingSelected) + .Where(y => y >= 0) + .Subscribe(_ => DoSortingSelected()); - this.WhenAnyValue( - x => x.AutoRefresh, - y => y == true) - .Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; }); + this.WhenAnyValue(x => x.AutoRefresh) + .Where(y => y) + .Subscribe(_ => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; }); #endregion WhenAnyValue && ReactiveCommand - _ = Init(); + _ = Task.Factory.StartNew( + async () => await GetClashProxiesTask(), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default + ); } - private async Task Init() - { - await DelayTestTask(); - } + public BulkObservableCollection ProxyGroups { get; } = []; + public BulkObservableCollection ProxyDetails { get; } = []; - private async Task DoRuleModeSelected(bool c) - { - if (!c) - { - return; - } - if (_config.ClashUIItem.RuleMode == (ERuleMode)RuleModeSelected) - { - return; - } - await SetRuleModeCheck((ERuleMode)RuleModeSelected); - } + public BulkObservableCollection ClashModes { get; } = new(Enum.GetNames().ToList()); - public async Task SetRuleModeCheck(ERuleMode mode) - { - if (_config.ClashUIItem.RuleMode == mode) - { - return; - } - await SetRuleMode(mode); - } + [Reactive] public partial ClashProxyModel? SelectedGroup { get; set; } - private void DoSortingSelected(bool c) + [Reactive] public partial ClashProxyModel? SelectedDetail { get; set; } + + public ReactiveCommand ProxiesReloadCmd { get; } + public ReactiveCommand ProxyDelayTestCmd { get; } + public ReactiveCommand GroupProxiesDelayTestCmd { get; } + public ReactiveCommand ProxiesSelectActivityCmd { get; } + + [Reactive] public partial string RuleModeSelected { get; set; } + + [Reactive] public partial int SortingSelected { get; set; } + + [Reactive] public partial bool AutoRefresh { get; set; } + + private void DoSortingSelected() { - if (!c) - { - return; - } if (SortingSelected != _config.ClashUIItem.ProxiesSorting) { _config.ClashUIItem.ProxiesSorting = SortingSelected; } - RefreshProxyDetails(c); + RefreshProxyDetails(); } public async Task ProxiesReload() { - await GetClashProxies(true); - await ProxiesDelayTest(); + await GetClashProxies(); + await GetClashModes(); } + #region task + + public async Task GetClashProxiesTask() + { + var numOfExecuted = 1; + while (true) + { + await Task.Delay(1000 * 60); + numOfExecuted++; + if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && + AppManager.Instance.IsRunningCore(ECoreType.sing_box))) + { + continue; + } + if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0) + { + continue; + } + if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0) + { + continue; + } + await GetClashProxies(); + } + } + + #endregion task + #region proxy function - private async Task SetRuleMode(ERuleMode mode) + private async Task SetRuleMode(string mode) { - _config.ClashUIItem.RuleMode = mode; - - if (mode != ERuleMode.Unchanged) - { - Dictionary headers = new() - { - { "mode", mode.ToString().ToLower() } - }; - await ClashApiManager.Instance.ClashConfigUpdate(headers); - } + await ClashApiManager.Instance.UpdateClashMode(mode); } - private async Task GetClashProxies(bool refreshUI) + private async Task GetClashProxies() { - var ret = await ClashApiManager.Instance.GetClashProxiesAsync(); - if (ret?.Item1 == null || ret.Item2 == null) + var ret = await ClashApiManager.Instance.GetProxies(); + if (ret?.IsEmpty() != false) { return; } - _proxies = ret.Item1.proxies; - _providers = ret?.Item2.providers; + _clashItem = ret; - if (refreshUI) - { - RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups()); - } + RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups()); } public async Task RefreshProxyGroups() { - if (_proxies == null) + if (_clashItem.IsEmpty()) { return; } var selectedName = SelectedGroup?.Name; - ProxyGroups.Clear(); var lstProxyGroups = new List(); - var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups(); - if (proxyGroups is { Count: > 0 }) - { - foreach (var it in proxyGroups) - { - if (it.name.IsNullOrEmpty() || !_proxies.TryGetValue(it.name, out var item)) - { - continue; - } - if (!Global.allowSelectType.Contains(item.type.ToLower())) - { - continue; - } - lstProxyGroups.Add(new ClashProxyModel() - { - Now = item.now, - Name = item.name, - Type = item.type - }); - } - } - //from api - foreach (var kv in _proxies) + var globalName = "GLOBAL"; + foreach (var kv in _clashItem.Proxies) { if (!Global.allowSelectType.Contains(kv.Value.type?.ToLower())) { continue; } - if (kv.Key == "GLOBAL") + if (kv.Key == globalName) { continue; } @@ -215,61 +173,49 @@ public partial class ClashProxiesViewModel : MyReactiveObject { continue; } - lstProxyGroups.Add(new ClashProxyModel() + lstProxyGroups.Add(new ClashProxyModel { Now = kv.Value.now, Name = kv.Key, Type = kv.Value.type, }); } - if (_proxies.TryGetValue("GLOBAL", out var globalProxy)) + if (_clashItem.Proxies.TryGetValue(globalName, out var globalProxy)) { - lstProxyGroups.Add(new ClashProxyModel() + lstProxyGroups.Add(new ClashProxyModel { Now = globalProxy.now, - Name = "GLOBAL", + Name = globalName, Type = globalProxy.type, }); } - ProxyGroups.AddRange(lstProxyGroups); + ProxyGroups.ReplaceRange(lstProxyGroups); if (ProxyGroups is { Count: > 0 }) { - if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName)) - { - SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName); - } - else - { - SelectedGroup = ProxyGroups.First(); - } + SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName) ?? ProxyGroups.First(); } else { - SelectedGroup = new(); + SelectedGroup = null; } await Task.CompletedTask; } - private void RefreshProxyDetails(bool c) + private void RefreshProxyDetails() { - ProxyDetails.Clear(); - if (!c) - { - return; - } var name = SelectedGroup?.Name; if (name.IsNullOrEmpty()) { return; } - if (_proxies == null) + if (_clashItem.IsEmpty()) { return; } - _proxies.TryGetValue(name, out var proxy); + _clashItem.Proxies.TryGetValue(name, out var proxy); if (proxy?.all == null) { return; @@ -284,7 +230,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject } var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1; - lstDetails.Add(new ClashProxyModel() + lstDetails.Add(new ClashProxyModel { IsActive = item == proxy.now, Name = item, @@ -293,7 +239,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject DelayName = delay <= 0 ? string.Empty : $"{delay}ms", }); } - //sort + // sort switch (SortingSelected) { case 0: @@ -303,101 +249,95 @@ public partial class ClashProxiesViewModel : MyReactiveObject case 1: lstDetails = lstDetails.OrderBy(t => t.Name).ToList(); break; - - default: - break; } - ProxyDetails.AddRange(lstDetails); + ProxyDetails.ReplaceRange(lstDetails); } - private ProxiesItem? TryGetProxy(string name) + private ClashProxy? TryGetProxy(string? name) { - if (_proxies == null) + if (name.IsNullOrEmpty()) { return null; } - _proxies.TryGetValue(name, out var proxy2); - if (proxy2 != null) - { - return proxy2; - } - //from providers - if (_providers != null) - { - foreach (var kv in _providers) - { - if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower())) - { - var proxy3 = kv.Value.proxies.FirstOrDefault(t => t.name == name); - if (proxy3 != null) - { - return proxy3; - } - } - } - } - return null; + _clashItem.Proxies.TryGetValue(name, out var proxy2); + return proxy2; } public async Task SetActiveProxy() { - if (SelectedGroup == null || SelectedGroup.Name.IsNullOrEmpty()) + if (SelectedGroup.Name.IsNullOrEmpty()) { return; } - if (SelectedDetail == null || SelectedDetail.Name.IsNullOrEmpty()) + if (SelectedDetail.Name.IsNullOrEmpty()) { return; } - var name = SelectedGroup.Name; - if (name.IsNullOrEmpty()) + var groupName = SelectedGroup.Name; + if (groupName.IsNullOrEmpty()) { return; } - var nameNode = SelectedDetail.Name; - if (nameNode.IsNullOrEmpty()) + var nodeName = SelectedDetail.Name; + if (nodeName.IsNullOrEmpty()) { return; } - var selectedProxy = TryGetProxy(name); - if (selectedProxy == null || selectedProxy.type != "Selector") + var selectedProxy = TryGetProxy(groupName); + if (selectedProxy is not { type: "Selector" }) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } - await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode); - - selectedProxy.now = nameNode; - var group = ProxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name); - if (group != null) - { - group.Now = nameNode; - var group2 = JsonUtils.DeepCopy(group); - ProxyGroups.Replace(group, group2); - - SelectedGroup = group2; - } + await ClashApiManager.Instance.SetActiveProxy(groupName, nodeName); + await GetClashProxies(); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } - private async Task ProxiesDelayTest(bool blAll = true) + private async Task GetClashModes() { - ClashApiManager.Instance.ClashProxiesDelayTest(blAll, ProxyDetails.ToList(), async (item, result) => + var ret = await ClashApiManager.Instance.GetClashModes(); + if (ret is not { Count: > 0 }) { - if (item == null || result.IsNullOrEmpty()) - { - return; - } + return; + } + ClashModes.ReplaceRange(ret); + var currentMode = await ClashApiManager.Instance.GetClashMode(); + if (currentMode.IsNullOrEmpty()) + { + return; + } + RuleModeSelected = currentMode; + } - var model = new SpeedTestResult() { IndexId = item.Name, Delay = result }; - RxSchedulers.MainThreadScheduler.Schedule(() => - { - _ = ProxiesDelayTestResult(model); - }); - await Task.CompletedTask; + private async Task TestProxyDelay(string name) + { + var result = await ClashApiManager.Instance.TestDelay(name, _clashItem); + var model = new SpeedTestResult + { + IndexId = name, + Delay = result.ToString(), + }; + await ProxiesDelayTestResult(model); + } + + private async Task TestGroupProxiesDelay() + { + var groupProxy = TryGetProxy(SelectedGroup.Name); + if (!Global.allowSelectType.Contains(groupProxy?.type)) + { + return; + } + + var options = new ParallelOptions + { + MaxDegreeOfParallelism = 4, + }; + await Parallel.ForEachAsync(groupProxy?.all ?? [], options, async (name, _) => + { + await TestProxyDelay(name); }); - await Task.CompletedTask; } public async Task ProxiesDelayTestResult(SpeedTestResult result) @@ -407,56 +347,10 @@ public partial class ClashProxiesViewModel : MyReactiveObject { return; } - - var dicResult = JsonUtils.Deserialize>(result.Delay); - if (dicResult != null && dicResult.TryGetValue("delay", out var value)) - { - detail.Delay = Convert.ToInt32(value.ToString()); - detail.DelayName = $"{detail.Delay}ms"; - } - else if (dicResult != null && dicResult.TryGetValue("message", out var value1)) - { - detail.Delay = _delayTimeout; - detail.DelayName = $"{value1}"; - } - else - { - detail.Delay = _delayTimeout; - detail.DelayName = string.Empty; - } + detail.Delay = Convert.ToInt32(result.Delay); + detail.DelayName = $"{detail.Delay}ms"; await Task.CompletedTask; } #endregion proxy function - - #region task - - public async Task DelayTestTask() - { - _ = Task.Run(async () => - { - var numOfExecuted = 1; - while (true) - { - await Task.Delay(1000 * 60); - numOfExecuted++; - if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && AppManager.Instance.IsRunningCore(ECoreType.sing_box))) - { - continue; - } - if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0) - { - continue; - } - if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0) - { - continue; - } - await ProxiesDelayTest(); - } - }); - await Task.CompletedTask; - } - - #endregion task } diff --git a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml index e82d2e3e..067b15ef 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml @@ -32,9 +32,11 @@ x:Name="cmbRulemode" Width="100" Margin="{StaticResource MarginLr8}"> - - - + + + + + @@ -141,7 +143,7 @@ Style="{StaticResource MaterialDesignListView}"> - + vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ProxiesReloadCmd, v => v.menuProxiesReload).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestCmd, v => v.menuProxiesDelaytest).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GroupProxiesDelayTestCmd, v => v.menuGroupProxiesDelaytest).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestPartCmd, v => v.menuProxiesDelaytestPart).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ProxyDelayTestCmd, v => v.menuProxyDelaytest).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ProxiesSelectActivityCmd, v => v.menuProxiesSelectActivity).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedIndex).DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.ClashModes, v => v.cmbRulemode.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); });