diff --git a/src/OpenClaw.Shared/NotificationCategorizer.cs b/src/OpenClaw.Shared/NotificationCategorizer.cs index e832638..b63e2b8 100644 --- a/src/OpenClaw.Shared/NotificationCategorizer.cs +++ b/src/OpenClaw.Shared/NotificationCategorizer.cs @@ -51,16 +51,22 @@ public class NotificationCategorizer /// /// Classify a notification using the layered pipeline. + /// When is true (default), + /// structured metadata (Intent, Channel) is checked first. + /// When false, classification starts from user-defined rules then keyword fallback. /// - public (string title, string type) Classify(OpenClawNotification notification, IReadOnlyList? userRules = null) + public (string title, string type) Classify(OpenClawNotification notification, IReadOnlyList? userRules = null, bool preferStructuredCategories = true) { - // 1. Structured metadata: Intent - if (!string.IsNullOrEmpty(notification.Intent) && IntentMap.TryGetValue(notification.Intent, out var intentResult)) - return intentResult; + if (preferStructuredCategories) + { + // 1. Structured metadata: Intent + if (!string.IsNullOrEmpty(notification.Intent) && IntentMap.TryGetValue(notification.Intent, out var intentResult)) + return intentResult; - // 2. Structured metadata: Channel - if (!string.IsNullOrEmpty(notification.Channel) && ChannelMap.TryGetValue(notification.Channel, out var channelResult)) - return channelResult; + // 2. Structured metadata: Channel + if (!string.IsNullOrEmpty(notification.Channel) && ChannelMap.TryGetValue(notification.Channel, out var channelResult)) + return channelResult; + } // 3. User-defined rules (pattern match on title + message) if (userRules is { Count: > 0 }) diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs index f47a984..ebe8e34 100644 --- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs +++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs @@ -60,6 +60,13 @@ private enum SignatureTokenMode private bool _operatorReadScopeUnavailable; private bool _pairingRequiredAwaitingApproval; private IReadOnlyList? _userRules; + private bool _preferStructuredCategories = true; + + /// + /// Controls whether structured notification metadata (Intent, Channel) takes priority + /// over keyword-based classification. Call after construction and whenever settings change. + /// + public void SetPreferStructuredCategories(bool value) => _preferStructuredCategories = value; private void ResetUnsupportedMethodFlags() { @@ -2005,7 +2012,7 @@ private void EmitNotification(string text) { Message = text.Length > 200 ? text[..200] + "…" : text }; - var (title, type) = _categorizer.Classify(notification, _userRules); + var (title, type) = _categorizer.Classify(notification, _userRules, _preferStructuredCategories); notification.Title = title; notification.Type = type; NotificationReceived?.Invoke(this, notification); diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 897358b..b895961 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -1103,6 +1103,7 @@ private void InitializeGatewayClient() _gatewayClient = new OpenClawGatewayClient(_settings.GetEffectiveGatewayUrl(), _settings.Token, new AppLogger()); _gatewayClient.SetUserRules(_settings.UserRules.Count > 0 ? _settings.UserRules : null); + _gatewayClient.SetPreferStructuredCategories(_settings.PreferStructuredCategories); _gatewayClient.StatusChanged += OnConnectionStatusChanged; _gatewayClient.ActivityChanged += OnActivityChanged; _gatewayClient.NotificationReceived += OnNotificationReceived; diff --git a/tests/OpenClaw.Shared.Tests/NotificationCategorizerTests.cs b/tests/OpenClaw.Shared.Tests/NotificationCategorizerTests.cs index bb2354c..556c3c3 100644 --- a/tests/OpenClaw.Shared.Tests/NotificationCategorizerTests.cs +++ b/tests/OpenClaw.Shared.Tests/NotificationCategorizerTests.cs @@ -272,6 +272,52 @@ public void PipelineOrder_Intent_Channel_UserRules_Keywords() Assert.Equal("health", _categorizer.Classify(notification).type); } + // --- PreferStructuredCategories = false --- + + [Fact] + public void PreferStructuredCategories_False_SkipsIntent() + { + var notification = new OpenClawNotification { Message = "New email notification", Intent = "build" }; + var (_, type) = _categorizer.Classify(notification, preferStructuredCategories: false); + Assert.Equal("email", type); + } + + [Fact] + public void PreferStructuredCategories_False_SkipsChannel() + { + var notification = new OpenClawNotification { Message = "Check your email", Channel = "calendar" }; + var (_, type) = _categorizer.Classify(notification, preferStructuredCategories: false); + Assert.Equal("email", type); + } + + [Fact] + public void PreferStructuredCategories_False_UserRulesStillApply() + { + var rules = new List + { + new() { Pattern = "invoice", Category = "email", Enabled = true } + }; + var notification = new OpenClawNotification { Message = "New invoice received", Intent = "urgent" }; + var (_, type) = _categorizer.Classify(notification, rules, preferStructuredCategories: false); + Assert.Equal("email", type); + } + + [Fact] + public void PreferStructuredCategories_False_FallsBackToKeywords() + { + var notification = new OpenClawNotification { Message = "Hello world", Intent = "build", Channel = "email" }; + var (_, type) = _categorizer.Classify(notification, preferStructuredCategories: false); + Assert.Equal("info", type); + } + + [Fact] + public void PreferStructuredCategories_True_Default_BehaviourUnchanged() + { + var notification = new OpenClawNotification { Message = "New email notification", Intent = "build" }; + Assert.Equal("build", _categorizer.Classify(notification).type); + Assert.Equal("build", _categorizer.Classify(notification, preferStructuredCategories: true).type); + } + // --- ClassifyByKeywords static method --- [Fact]