feat(app): 初始化 OmniNotify 测试工具

创建基于 .NET 8 WPF 的桌面测试程序,支持配置通知接口、频道、标题和正文,并通过内置模板快速发送不同类型的测试消息。

实现服务检测、单次发送、自动定时发送、模板轮换、批量发送、非法频道切换和发送日志记录,便于验证 OmniNotify 本地消息 API 的基础行为与异常场景。

Initial-Commit: true
This commit is contained in:
2026-05-19 01:30:40 +08:00
commit 658154aca7
8 changed files with 521 additions and 0 deletions

261
MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,261 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Threading;
namespace OmniNotifyTest;
public partial class MainWindow : Window, INotifyPropertyChanged
{
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(4) };
private readonly DispatcherTimer _autoTimer = new();
private int _autoSent;
private int _presetIndex;
private MessagePreset? _selectedPreset;
private string _statusText = "准备就绪。请确认 OmniNotify 正在运行,且目标频道已在主程序中创建。";
public MainWindow()
{
InitializeComponent();
DataContext = this;
Channels.Add("default");
Channels.Add("build");
Channels.Add("monitor");
Channels.Add("urgent");
Presets.Add(new MessagePreset("普通消息", "测试通知", "这是一条来自 OmniNotify 测试器的普通消息。"));
Presets.Add(new MessagePreset("长正文", "长文本排版测试", "第一行:用于观察弹窗宽度、最大高度和换行。\r\n第二行这段正文会稍微长一些用来测试截断、跑马灯或拆分显示。\r\n第三行消息发送时间 {time}。"));
Presets.Add(new MessagePreset("空标题", "", "只有正文,没有标题。"));
Presets.Add(new MessagePreset("构建成功", "Build finished", "Release 构建完成,所有测试通过。"));
Presets.Add(new MessagePreset("告警", "CPU 使用率过高", "monitor 节点检测到 CPU 持续高于 90%,请检查后台任务。"));
Presets.Add(new MessagePreset("特殊字符", "Symbols !@#$%", "中文、English、数字 12345以及 JSON 转义字符:\" \\ /。"));
SelectedPreset = Presets[0];
ApplyPreset(SelectedPreset);
_autoTimer.Tick += async (_, _) => await SendAutoTickAsync();
}
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<string> Channels { get; } = [];
public ObservableCollection<MessagePreset> Presets { get; } = [];
public ObservableCollection<SendLogItem> Logs { get; } = [];
public string Endpoint { get; set; } = "http://127.0.0.1:19845/notify";
public string SelectedChannel { get; set; } = "default";
public string MessageTitle { get; set; } = "";
public string Body { get; set; } = "";
public int IntervalMilliseconds { get; set; } = 1000;
public int AutoLimit { get; set; } = 20;
public int BurstCount { get; set; } = 10;
public bool RotatePresets { get; set; } = true;
public string StatusText
{
get => _statusText;
set => SetField(ref _statusText, value);
}
public MessagePreset? SelectedPreset
{
get => _selectedPreset;
set => SetField(ref _selectedPreset, value);
}
private async void CheckServer_Click(object sender, RoutedEventArgs e)
{
try
{
var baseUri = new Uri(Endpoint);
var root = new Uri(baseUri.GetLeftPart(UriPartial.Authority));
var response = await Http.GetAsync(root);
StatusText = response.IsSuccessStatusCode
? $"服务可达:{root}"
: $"服务响应异常HTTP {(int)response.StatusCode}";
}
catch (Exception ex)
{
StatusText = $"服务不可达:{ex.Message}";
}
}
private async void SendOnce_Click(object sender, RoutedEventArgs e)
{
await SendCurrentAsync("手动");
}
private void StartAuto_Click(object sender, RoutedEventArgs e)
{
_autoSent = 0;
_autoTimer.Interval = TimeSpan.FromMilliseconds(Math.Clamp(IntervalMilliseconds, 100, 60000));
_autoTimer.Start();
StatusText = $"自动发送已开始:间隔 {_autoTimer.Interval.TotalMilliseconds:0} ms。";
}
private void StopAuto_Click(object sender, RoutedEventArgs e)
{
StopAuto("自动发送已停止。");
}
private async void Burst_Click(object sender, RoutedEventArgs e)
{
var count = Math.Clamp(BurstCount, 1, 200);
StatusText = $"开始批量发送 {count} 条消息。";
for (var i = 0; i < count; i++)
{
if (RotatePresets)
{
ApplyNextPreset();
}
await SendCurrentAsync("批量", i + 1);
}
StatusText = $"批量发送完成:{count} 条。";
}
private void AddChannel_Click(object sender, RoutedEventArgs e)
{
var channel = string.IsNullOrWhiteSpace(SelectedChannel) ? "default" : SelectedChannel.Trim();
if (!Channels.Contains(channel))
{
Channels.Add(channel);
}
SelectedChannel = channel;
OnPropertyChanged(nameof(SelectedChannel));
}
private void UseIllegalChannel_Click(object sender, RoutedEventArgs e)
{
SelectedChannel = $"illegal-{DateTime.Now:HHmmss}";
OnPropertyChanged(nameof(SelectedChannel));
StatusText = "已切换到一个通常不存在的频道,可用于测试 IllegalChannel。";
}
private void ClearLog_Click(object sender, RoutedEventArgs e)
{
Logs.Clear();
StatusText = "日志已清空。";
}
private void Preset_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (SelectedPreset is not null)
{
ApplyPreset(SelectedPreset);
}
}
private async Task SendAutoTickAsync()
{
if (AutoLimit > 0 && _autoSent >= AutoLimit)
{
StopAuto($"自动发送完成:{_autoSent} 条。");
return;
}
if (RotatePresets)
{
ApplyNextPreset();
}
_autoSent++;
await SendCurrentAsync("自动", _autoSent);
}
private async Task SendCurrentAsync(string mode, int? sequence = null)
{
var channel = string.IsNullOrWhiteSpace(SelectedChannel) ? "default" : SelectedChannel.Trim();
var message = new NotifyMessage(
channel,
ExpandTokens(MessageTitle, sequence),
ExpandTokens(Body, sequence));
try
{
var response = await Http.PostAsJsonAsync(Endpoint, message);
var result = await response.Content.ReadAsStringAsync();
AddLog(channel, message.Title, (int)response.StatusCode, string.IsNullOrWhiteSpace(result) ? response.ReasonPhrase ?? mode : result);
StatusText = $"{mode}发送完成HTTP {(int)response.StatusCode} {response.ReasonPhrase}";
}
catch (Exception ex)
{
AddLog(channel, message.Title, 0, ex.Message);
StatusText = $"{mode}发送失败:{ex.Message}";
}
}
private void ApplyPreset(MessagePreset preset)
{
MessageTitle = preset.Title;
Body = preset.Body;
OnPropertyChanged(nameof(MessageTitle));
OnPropertyChanged(nameof(Body));
}
private void ApplyNextPreset()
{
if (Presets.Count == 0)
{
return;
}
_presetIndex = (_presetIndex + 1) % Presets.Count;
SelectedPreset = Presets[_presetIndex];
ApplyPreset(SelectedPreset);
}
private string ExpandTokens(string value, int? sequence)
{
return value
.Replace("{time}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{seq}", (sequence ?? Logs.Count + 1).ToString());
}
private void StopAuto(string message)
{
_autoTimer.Stop();
StatusText = message;
}
private void AddLog(string channel, string title, int statusCode, string result)
{
Logs.Insert(0, new SendLogItem(DateTime.Now, channel, title, statusCode == 0 ? "-" : statusCode.ToString(), result));
while (Logs.Count > 500)
{
Logs.RemoveAt(Logs.Count - 1);
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return;
}
field = value;
OnPropertyChanged(propertyName);
}
}
public sealed record MessagePreset(string Name, string Title, string Body);
public sealed record NotifyMessage(string Channel, string Title, string Body);
public sealed record SendLogItem(DateTime Time, string Channel, string Title, string StatusCode, string Result)
{
public string TimeText => Time.ToString("HH:mm:ss");
}