Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/OpenClaw.Shared/NotificationCategorizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,22 @@ public class NotificationCategorizer

/// <summary>
/// Classify a notification using the layered pipeline.
/// When <paramref name="preferStructuredCategories"/> is true (default),
/// structured metadata (Intent, Channel) is checked first.
/// When false, classification starts from user-defined rules then keyword fallback.
/// </summary>
public (string title, string type) Classify(OpenClawNotification notification, IReadOnlyList<UserNotificationRule>? userRules = null)
public (string title, string type) Classify(OpenClawNotification notification, IReadOnlyList<UserNotificationRule>? 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 })
Expand Down
9 changes: 8 additions & 1 deletion src/OpenClaw.Shared/OpenClawGatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ private enum SignatureTokenMode
private bool _operatorReadScopeUnavailable;
private bool _pairingRequiredAwaitingApproval;
private IReadOnlyList<UserNotificationRule>? _userRules;
private bool _preferStructuredCategories = true;

/// <summary>
/// Controls whether structured notification metadata (Intent, Channel) takes priority
/// over keyword-based classification. Call after construction and whenever settings change.
/// </summary>
public void SetPreferStructuredCategories(bool value) => _preferStructuredCategories = value;

private void ResetUnsupportedMethodFlags()
{
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@
StartDeepLinkServer();

// Register global hotkey if enabled
if (_settings.GlobalHotkeyEnabled)

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build-msix (win-x64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 304 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build-msix (win-arm64)

Dereference of a possibly null reference.
{
_globalHotkey = new GlobalHotkeyService();
_globalHotkey.HotkeyPressed += OnGlobalHotkeyPressed;
Expand Down Expand Up @@ -1103,6 +1103,7 @@

_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;
Expand Down
46 changes: 46 additions & 0 deletions tests/OpenClaw.Shared.Tests/NotificationCategorizerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserNotificationRule>
{
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]
Expand Down
Loading