Skip to content
Open
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
82 changes: 31 additions & 51 deletions src/OpenClaw.Companion/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,59 +1,39 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml;
using OpenClaw.Companion.ViewModels;
using OpenClaw.Companion.Views;
using OpenClaw.Companion.Services;

namespace OpenClaw.Companion;

public partial class App : Application
{
private GatewayWebSocketClient? _client;

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();

_client = new GatewayWebSocketClient();
var settings = new SettingsStore();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(settings, _client),
};

desktop.Exit += async (_, _) =>
{
if (_client is not null)
await _client.DisposeAsync();
};
}

base.OnFrameworkInitializationCompleted();
}
using Avalonia.Markup.Xaml;
using OpenClaw.Companion.ViewModels;
using OpenClaw.Companion.Views;
using OpenClaw.Companion.Services;

private void DisableAvaloniaDataAnnotationValidation()
namespace OpenClaw.Companion;

public partial class App : Application
{
private GatewayWebSocketClient? _client;

public override void Initialize()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
AvaloniaXamlLoader.Load(this);
}

// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
BindingPlugins.DataValidators.Remove(plugin);
_client = new GatewayWebSocketClient();
var settings = new SettingsStore();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(settings, _client),
};

desktop.Exit += async (_, _) =>
{
if (_client is not null)
await _client.DisposeAsync();
};
}

base.OnFrameworkInitializationCompleted();
}
}
}
11 changes: 6 additions & 5 deletions src/OpenClaw.Companion/OpenClaw.Companion.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@

<ItemGroup>
<ProjectReference Include="..\OpenClaw.Client\OpenClaw.Client.csproj" />
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<PackageReference Include="Avalonia" Version="11.3.13" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.13" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.13" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.13" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.13">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.21.3" />
</ItemGroup>
</Project>
95 changes: 51 additions & 44 deletions src/OpenClaw.Gateway/Endpoints/AdminEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1380,29 +1380,7 @@ operations.ModelProfiles as ConfiguredModelProfileRegistry
using var subscription = authEventStore.Subscribe();
var ct = ctx.RequestAborted;

// Send current state as first event
var currentItems = accountId is not null
? authEventStore.GetLatest(channelId, accountId) is { } currentEvt ? [currentEvt] : []
: authEventStore.GetAll(channelId);
foreach (var current in currentItems)
{
var json = JsonSerializer.Serialize(MapChannelAuthStatusItem(current), CoreJsonContext.Default.ChannelAuthStatusItem);
await ctx.Response.WriteAsync($"data: {json}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}

// Stream subsequent events
await foreach (var evt in subscription.Reader.ReadAllAsync(ct))
{
if (!string.Equals(evt.ChannelId, channelId, StringComparison.Ordinal))
continue;
if (accountId is not null && !string.Equals(evt.AccountId, accountId, StringComparison.Ordinal))
continue;

var evtJson = JsonSerializer.Serialize(MapChannelAuthStatusItem(evt), CoreJsonContext.Default.ChannelAuthStatusItem);
await ctx.Response.WriteAsync($"data: {evtJson}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}
await StreamChannelAuthEventsAsync(ctx, subscription, authEventStore, channelId, accountId, ct);
});

app.MapGet("/admin/channels/whatsapp/auth", (HttpContext ctx, string? accountId) =>
Expand Down Expand Up @@ -1439,27 +1417,7 @@ operations.ModelProfiles as ConfiguredModelProfileRegistry
using var subscription = authEventStore.Subscribe();
var ct = ctx.RequestAborted;

var currentItems = accountId is not null
? authEventStore.GetLatest("whatsapp", accountId) is { } currentEvt ? [currentEvt] : []
: authEventStore.GetAll("whatsapp");
foreach (var current in currentItems)
{
var json = JsonSerializer.Serialize(MapChannelAuthStatusItem(current), CoreJsonContext.Default.ChannelAuthStatusItem);
await ctx.Response.WriteAsync($"data: {json}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}

await foreach (var evt in subscription.Reader.ReadAllAsync(ct))
{
if (!string.Equals(evt.ChannelId, "whatsapp", StringComparison.Ordinal))
continue;
if (accountId is not null && !string.Equals(evt.AccountId, accountId, StringComparison.Ordinal))
continue;

var evtJson = JsonSerializer.Serialize(MapChannelAuthStatusItem(evt), CoreJsonContext.Default.ChannelAuthStatusItem);
await ctx.Response.WriteAsync($"data: {evtJson}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}
await StreamChannelAuthEventsAsync(ctx, subscription, authEventStore, "whatsapp", accountId, ct);
});

app.MapGet("/admin/channels/whatsapp/auth/qr.svg", (HttpContext ctx, string? accountId) =>
Expand Down Expand Up @@ -1739,6 +1697,55 @@ private static ChannelAuthStatusItem MapChannelAuthStatusItem(BridgeChannelAuthE
UpdatedAtUtc = evt.UpdatedAtUtc
};

private static async Task StreamChannelAuthEventsAsync(
HttpContext ctx,
ChannelAuthEventStore.AuthEventSubscription subscription,
ChannelAuthEventStore authEventStore,
string channelId,
string? accountId,
CancellationToken ct)
{
try
{
await ctx.Response.WriteAsync(": connected\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);

var currentItems = accountId is not null
? authEventStore.GetLatest(channelId, accountId) is { } currentEvt ? [currentEvt] : []
: authEventStore.GetAll(channelId);
foreach (var current in currentItems)
{
await WriteChannelAuthSseEventAsync(ctx, current, ct);
}

await foreach (var evt in subscription.Reader.ReadAllAsync(ct))
{
if (!string.Equals(evt.ChannelId, channelId, StringComparison.Ordinal))
continue;
if (accountId is not null && !string.Equals(evt.AccountId, accountId, StringComparison.Ordinal))
continue;

await WriteChannelAuthSseEventAsync(ctx, evt, ct);
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
return;
}
}

private static Task WriteChannelAuthSseEventAsync(HttpContext ctx, BridgeChannelAuthEvent evt, CancellationToken ct)
{
var json = JsonSerializer.Serialize(MapChannelAuthStatusItem(evt), CoreJsonContext.Default.ChannelAuthStatusItem);
return WriteSsePayloadAsync(ctx, $"data: {json}\n\n", ct);
}

private static async Task WriteSsePayloadAsync(HttpContext ctx, string payload, CancellationToken ct)
{
await ctx.Response.WriteAsync(payload, ct);
await ctx.Response.Body.FlushAsync(ct);
}

private static WhatsAppSetupResponse BuildWhatsAppSetupResponse(
GatewayStartupContext startup,
GatewayAppRuntime runtime,
Expand Down
68 changes: 34 additions & 34 deletions src/OpenClaw.Gateway/GatewayLlmExecutionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,15 @@ private sealed class RouteState

public GatewayLlmExecutionService(
GatewayConfig config,
ConfiguredModelProfileRegistry modelProfiles,
IModelSelectionPolicy selectionPolicy,
LlmProviderRegistry providerRegistry,
ProviderPolicyService policyService,
RuntimeEventStore eventStore,
RuntimeMetrics runtimeMetrics,
ProviderUsageTracker providerUsage,
ILogger<GatewayLlmExecutionService> logger)
: this(
config,
modelProfiles,
selectionPolicy,
CreateCompatibilityServices(config, providerRegistry),
policyService,
eventStore,
runtimeMetrics,
Expand All @@ -67,39 +65,40 @@ public GatewayLlmExecutionService(

public GatewayLlmExecutionService(
GatewayConfig config,
ConfiguredModelProfileRegistry modelProfiles,
IModelSelectionPolicy selectionPolicy,
LlmProviderRegistry providerRegistry,
ProviderPolicyService policyService,
RuntimeEventStore eventStore,
RuntimeMetrics runtimeMetrics,
ProviderUsageTracker providerUsage,
PromptCacheCoordinator promptCacheCoordinator,
PromptCacheWarmRegistry promptCacheWarmRegistry,
ILogger<GatewayLlmExecutionService> logger)
: this(
config,
CreateCompatibilityServices(config, providerRegistry),
policyService,
eventStore,
runtimeMetrics,
providerUsage,
promptCacheCoordinator,
promptCacheWarmRegistry,
logger)
{
_config = config;
_modelProfiles = modelProfiles;
_selectionPolicy = selectionPolicy;
_policyService = policyService;
_eventStore = eventStore;
_runtimeMetrics = runtimeMetrics;
_providerUsage = providerUsage;
_promptCacheCoordinator = promptCacheCoordinator;
_promptCacheWarmRegistry = promptCacheWarmRegistry;
_logger = logger;
}

public GatewayLlmExecutionService(
GatewayConfig config,
LlmProviderRegistry registry,
ConfiguredModelProfileRegistry modelProfiles,
IModelSelectionPolicy selectionPolicy,
ProviderPolicyService policyService,
RuntimeEventStore eventStore,
RuntimeMetrics runtimeMetrics,
ProviderUsageTracker providerUsage,
ILogger<GatewayLlmExecutionService> logger)
: this(
config,
registry,
modelProfiles,
selectionPolicy,
policyService,
eventStore,
runtimeMetrics,
Expand All @@ -110,9 +109,9 @@ public GatewayLlmExecutionService(
{
}

public GatewayLlmExecutionService(
private GatewayLlmExecutionService(
GatewayConfig config,
LlmProviderRegistry registry,
CompatibilityServices compatibilityServices,
ProviderPolicyService policyService,
RuntimeEventStore eventStore,
RuntimeMetrics runtimeMetrics,
Expand All @@ -122,7 +121,8 @@ public GatewayLlmExecutionService(
ILogger<GatewayLlmExecutionService> logger)
: this(
config,
CreateCompatibilityServices(config, registry),
compatibilityServices.Registry,
compatibilityServices.SelectionPolicy,
policyService,
eventStore,
runtimeMetrics,
Expand All @@ -133,28 +133,28 @@ public GatewayLlmExecutionService(
{
}

private GatewayLlmExecutionService(
public GatewayLlmExecutionService(
GatewayConfig config,
CompatibilityServices compatibility,
ConfiguredModelProfileRegistry modelProfiles,
IModelSelectionPolicy selectionPolicy,
ProviderPolicyService policyService,
RuntimeEventStore eventStore,
RuntimeMetrics runtimeMetrics,
ProviderUsageTracker providerUsage,
PromptCacheCoordinator promptCacheCoordinator,
PromptCacheWarmRegistry promptCacheWarmRegistry,
ILogger<GatewayLlmExecutionService> logger)
: this(
config,
compatibility.Registry,
compatibility.SelectionPolicy,
policyService,
eventStore,
runtimeMetrics,
providerUsage,
promptCacheCoordinator,
promptCacheWarmRegistry,
logger)
{
_config = config;
_modelProfiles = modelProfiles;
_selectionPolicy = selectionPolicy;
_policyService = policyService;
_eventStore = eventStore;
_runtimeMetrics = runtimeMetrics;
_providerUsage = providerUsage;
_promptCacheCoordinator = promptCacheCoordinator;
_promptCacheWarmRegistry = promptCacheWarmRegistry;
_logger = logger;
}

public CircuitState DefaultCircuitState
Expand Down
Loading
Loading