fix conflicts
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v1.0.1 (2026-05-08)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **节点切换不彻底** — 切换节点后自动遍历并重路由所有引用该组的父级服务组(如 `🌍 国外媒体`、`📲 电报信息`),确保规则匹配的流量真正经过新代理,解决 "UI 和 Web 面板显示已切换,实际地区未改变" 的问题
|
||||||
|
- **节点选择不再依赖硬编码名称** — 改为关键字模糊匹配(`选择` `节点` `Proxy` `Auto` `Select` `自動`),适配各种命名习惯(`🔰 选择节点` `🌍 节点选择` 等),自动选代理选项最多的组
|
||||||
|
- **配置文件持久化修复** — 单文件发布版改用实际 EXE 目录保存 `config.json`,不再因临时解压路径变化丢失设置
|
||||||
|
- **下拉框闪烁修复** — `StaysOpen="True"` 防止点按后立即收回,`SelectionChanged` 事件替代 `MouseLeftButtonUp`
|
||||||
|
- **节点选中标记** — 绿色圆点标记当前使用的节点
|
||||||
|
- **窗口退出动画** — 淡入淡出过渡效果
|
||||||
|
- **代码清理** — 移除未使用的事件和死代码,修复不可靠的 `% 3` 节流逻辑
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **⚙ 设置窗口** — 可配置 API Host、Port、Secret Key、Test URL,保存即重连
|
||||||
|
- **设置窗毛玻璃统一风格** — 与主窗口一致的半透明白色毛玻璃美学
|
||||||
|
- **版本号显示** — 设置页底部标注 `Version 1.0.1`
|
||||||
|
- **输出文件重命名** — 完整版 `ClashWidget_v1.0.1_windows_amd64.exe`,轻量版 `ClashWidget_v1.0.1_windows_amd64_lite.exe`
|
||||||
|
- **轻量版运行时引导** — 缺 .NET 8 时自动打开下载页面
|
||||||
|
- **中文项目文档** — `README.md` 含功能介绍、快速开始、项目结构、配置说明
|
||||||
|
|
||||||
|
## v1.0.0 (初始版本)
|
||||||
|
|
||||||
|
- 悬浮毛玻璃窗口,实时显示 Clash Meta 下载/上传速度
|
||||||
|
- 300ms 刷新率,速度曲线图 (sparkline)
|
||||||
|
- 节点名显示,延迟测速(⚡ 手动触发)
|
||||||
|
- macOS 风格红绿灯窗口控件
|
||||||
|
- 始终置顶,可拖拽,隐藏任务栏
|
||||||
|
- 单文件 EXE 发布
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
### 下载运行
|
### 下载运行
|
||||||
|
|
||||||
从 [Releases](../../releases) 下载最新版本:
|
从 [Releases](https://git.gl-cloud.top/GuiLing/ClashWidget/releases) 下载最新版本:
|
||||||
|
|
||||||
- `ClashWidget.exe`(完整版,155 MB)— 无需安装 .NET,Windows 10 / 11 x64 双击运行
|
- `ClashWidget.exe`(完整版,155 MB)— 无需安装 .NET,Windows 10 / 11 x64 双击运行
|
||||||
- `ClashWidget-lite.exe`(轻量版,~230 KB)— 需安装 [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0)
|
- `ClashWidget-lite.exe`(轻量版,~230 KB)— 需安装 [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||||
|
|||||||
+71
-54
@@ -18,17 +18,23 @@ public class ClashApiService
|
|||||||
public ClashApiService(AppConfig config)
|
public ClashApiService(AppConfig config)
|
||||||
{
|
{
|
||||||
_testUrl = config.TestUrl;
|
_testUrl = config.TestUrl;
|
||||||
_tokenQuery = $"?token={Uri.EscapeDataString(config.ApiSecret)}";
|
|
||||||
|
bool hasAuth = !string.IsNullOrWhiteSpace(config.ApiSecret);
|
||||||
|
_tokenQuery = hasAuth ? $"?token={Uri.EscapeDataString(config.ApiSecret)}" : "";
|
||||||
|
|
||||||
_httpClient = new HttpClient
|
_httpClient = new HttpClient
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri(config.ApiBaseUrl),
|
BaseAddress = new Uri(config.ApiBaseUrl),
|
||||||
Timeout = TimeSpan.FromSeconds(5)
|
Timeout = TimeSpan.FromSeconds(5)
|
||||||
};
|
};
|
||||||
_httpClient.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("Bearer", config.ApiSecret);
|
if (hasAuth)
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new AuthenticationHeaderValue("Bearer", config.ApiSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string WithToken(string path) => $"{path}{_tokenQuery}";
|
private string WithToken(string path) =>
|
||||||
|
_tokenQuery.Length > 0 ? $"{path}{_tokenQuery}" : path;
|
||||||
|
|
||||||
public async Task<TrafficInfo?> GetTrafficAsync()
|
public async Task<TrafficInfo?> GetTrafficAsync()
|
||||||
{
|
{
|
||||||
@@ -100,29 +106,7 @@ public class ClashApiService
|
|||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||||
|
|
||||||
// First: ensure parent groups route through this proxy group
|
// 1) Switch the proxy in the target group
|
||||||
var proxies = await GetProxiesAsync();
|
|
||||||
if (proxies != null)
|
|
||||||
{
|
|
||||||
var skip = new HashSet<string> { "DIRECT", "REJECT", "REJECT-DROP" };
|
|
||||||
foreach (var g in proxies)
|
|
||||||
{
|
|
||||||
if (g.Type == "Selector" && g.All != null
|
|
||||||
&& g.All.Contains(groupName)
|
|
||||||
&& g.Now != null && skip.Contains(g.Now))
|
|
||||||
{
|
|
||||||
var parentPayload = JsonSerializer.Serialize(new { name = groupName });
|
|
||||||
using var parentContent = new StringContent(parentPayload,
|
|
||||||
System.Text.Encoding.UTF8, "application/json");
|
|
||||||
using var parentReq = new HttpRequestMessage(HttpMethod.Put,
|
|
||||||
WithToken($"/proxies/{Uri.EscapeDataString(g.Name)}"))
|
|
||||||
{ Content = parentContent };
|
|
||||||
await _httpClient.SendAsync(parentReq, cts.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then: switch the actual proxy within the group
|
|
||||||
var payload = JsonSerializer.Serialize(new { name = proxyName });
|
var payload = JsonSerializer.Serialize(new { name = proxyName });
|
||||||
using var content = new StringContent(payload,
|
using var content = new StringContent(payload,
|
||||||
System.Text.Encoding.UTF8, "application/json");
|
System.Text.Encoding.UTF8, "application/json");
|
||||||
@@ -131,19 +115,43 @@ public class ClashApiService
|
|||||||
{ Content = content };
|
{ Content = content };
|
||||||
using var response = await _httpClient.SendAsync(request, cts.Token);
|
using var response = await _httpClient.SendAsync(request, cts.Token);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode) return false;
|
||||||
{
|
|
||||||
// Force close all connections to reconnect through the new proxy
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var delReq = new HttpRequestMessage(HttpMethod.Delete,
|
|
||||||
WithToken("/connections"));
|
|
||||||
await _httpClient.SendAsync(delReq, cts.Token);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.IsSuccessStatusCode;
|
// 2) Reroute all parent service groups (🌍 国外媒体, 📲 电报 etc.)
|
||||||
|
// that reference this proxy group, so traffic flows through the selection
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var proxies = await GetProxiesAsync();
|
||||||
|
if (proxies != null)
|
||||||
|
{
|
||||||
|
foreach (var g in proxies)
|
||||||
|
{
|
||||||
|
if (g.Type == "Selector" && g.Name != groupName
|
||||||
|
&& g.All?.Contains(groupName) == true)
|
||||||
|
{
|
||||||
|
using var parentC = new StringContent(
|
||||||
|
JsonSerializer.Serialize(new { name = groupName }),
|
||||||
|
System.Text.Encoding.UTF8, "application/json");
|
||||||
|
using var parentReq = new HttpRequestMessage(HttpMethod.Put,
|
||||||
|
WithToken($"/proxies/{Uri.EscapeDataString(g.Name)}"))
|
||||||
|
{ Content = parentC };
|
||||||
|
await _httpClient.SendAsync(parentReq, cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
// 3) Force close all connections to reconnect through new path
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var del = new HttpRequestMessage(HttpMethod.Delete,
|
||||||
|
WithToken("/connections"));
|
||||||
|
await _httpClient.SendAsync(del, cts.Token);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -154,26 +162,35 @@ public class ClashApiService
|
|||||||
public (string? nodeName, string? groupName, List<string>? availableNodes) ExtractCurrentNode(
|
public (string? nodeName, string? groupName, List<string>? availableNodes) ExtractCurrentNode(
|
||||||
ProxyGroupResponse? groupResponse, List<ProxyEntry>? proxiesList)
|
ProxyGroupResponse? groupResponse, List<ProxyEntry>? proxiesList)
|
||||||
{
|
{
|
||||||
var skipValues = new HashSet<string> { "DIRECT", "REJECT", "REJECT-DROP" };
|
|
||||||
var priorityNames = new[] { "🔰 选择节点", "Proxy", "🚀 自动选择", "♻️ 自动故障转移" };
|
|
||||||
|
|
||||||
List<ProxyEntry>? entries = proxiesList ?? groupResponse?.Proxies;
|
List<ProxyEntry>? entries = proxiesList ?? groupResponse?.Proxies;
|
||||||
if (entries == null) return (null, null, null);
|
if (entries == null) return (null, null, null);
|
||||||
|
|
||||||
foreach (var name in priorityNames)
|
var skipValues = new HashSet<string> { "DIRECT", "REJECT", "REJECT-DROP" };
|
||||||
{
|
|
||||||
var match = entries.FirstOrDefault(p =>
|
|
||||||
p.Name == name && p.Now != null && !skipValues.Contains(p.Now));
|
|
||||||
if (match?.Now != null)
|
|
||||||
return (FixEmojiFlags(match.Now), match.Name, match.All);
|
|
||||||
}
|
|
||||||
|
|
||||||
var selector = entries.FirstOrDefault(p =>
|
// Keyword-based fuzzy matching for proxy selection groups.
|
||||||
p.Type == "Selector" && p.Now != null && !skipValues.Contains(p.Now));
|
// Works with: 🔰 选择节点 / 🌍 节点选择 / Proxy / 🚀 自动选择 / etc.
|
||||||
if (selector?.Now != null)
|
var keywords = new[] { "选择", "节点", "Proxy", "Auto", "Select", "自動" };
|
||||||
return (FixEmojiFlags(selector.Now), selector.Name, selector.All);
|
|
||||||
|
|
||||||
return (null, null, null);
|
var candidates = entries
|
||||||
|
.Where(p => p.Type == "Selector"
|
||||||
|
&& p.Now != null
|
||||||
|
&& !skipValues.Contains(p.Now)
|
||||||
|
&& p.All is { Count: > 0 })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (candidates.Count == 0) return (null, null, null);
|
||||||
|
|
||||||
|
// Prefer groups whose name contains at least one keyword,
|
||||||
|
// and among those, pick the one with the largest "all" list (richest proxy options).
|
||||||
|
var keywordMatches = candidates
|
||||||
|
.Where(p => keywords.Any(k => p.Name.Contains(k, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var best = keywordMatches.Count > 0
|
||||||
|
? keywordMatches.MaxBy(p => p.All!.Count)!
|
||||||
|
: candidates.MaxBy(p => p.All!.Count)!;
|
||||||
|
|
||||||
|
return (FixEmojiFlags(best.Now!), best.Name, best.All);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double> MeasureLatencyAsync()
|
public async Task<double> MeasureLatencyAsync()
|
||||||
|
|||||||
+8
-1
@@ -5,7 +5,7 @@
|
|||||||
Background="#AAFFFFFF"
|
Background="#AAFFFFFF"
|
||||||
Topmost="True"
|
Topmost="True"
|
||||||
ResizeMode="NoResize"
|
ResizeMode="NoResize"
|
||||||
Width="360" Height="370"
|
Width="360" Height="400"
|
||||||
MouseLeftButtonDown="OnMouseLeftButtonDown">
|
MouseLeftButtonDown="OnMouseLeftButtonDown">
|
||||||
|
|
||||||
<Grid Margin="20,12">
|
<Grid Margin="20,12">
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Row 0: traffic light + title -->
|
<!-- Row 0: traffic light + title -->
|
||||||
@@ -95,5 +96,11 @@
|
|||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Button.Template>
|
</Button.Template>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<!-- Version -->
|
||||||
|
<TextBlock Grid.Row="10" Text="Version 1.0.1"
|
||||||
|
FontSize="11" Foreground="#FF1E293B"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,16,0,0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
+3659
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user