Skip to main content

openclaw-windows-node

Build and extend the OpenClaw Windows companion suite - System Tray app, Node capabilities, CLI tools, and gateway integration

Zur Installation springen

Quellinformationen

Repository
reason-machines/hermes-skills
Letzte Quellaktivität
6. Juni 2026 um 14:34
Erkannte Sprache von SKILL.md
Englisch
Sterne
5
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
openclaw-windows-node
description
Build and extend the OpenClaw Windows companion suite - System Tray app, Node capabilities, CLI tools, and gateway integration
triggers
["how do I build the OpenClaw Windows tray app","add a new node capability to OpenClaw Windows","debug OpenClaw Windows gateway connection","create a custom notification in OpenClaw tray","extend the OpenClaw Windows command center","implement a new channel control in OpenClaw","troubleshoot OpenClaw Windows WebSocket issues","add MCP tool support to OpenClaw Windows node"]
# OpenClaw Windows Node > Skill by [ara.so](https://ara.so) — Hermes Skills collection. Windows companion suite for OpenClaw AI assistant. Provides a WinUI 3 system tray app (Molty), shared gateway client library, and CLI utilities. The tray app connects to the OpenClaw gateway via WebSocket, displays status/sessions/usage/channels/nodes, provides Quick Send, embedded Web Chat, toast notifications, and optional Node Mode for agent control of Windows. ## What It Does - **System Tray App** - WinUI 3 app with flyout menu, gateway status, sessions, channels, usage, nodes, Command Center diagnostics, auto-updates - **Node Capabilities** - When enabled, exposes Windows as a controllable node: `system.notify`, `system.run`, `canvas.present`, `screen.snapshot`, `camera.snap`, `stt.transcribe`, `tts.speak`, etc. - **Gateway Client** - Shared C# library for WebSocket connection, pairing, pub/sub, RPC - **CLI Validator** - Command-line tool to test gateway connectivity and `chat.send` - **Auto-Updates** - GitHub Releases integration with channel selection (stable/alpha/dev) - **Deep Links** - `openclaw://` URL scheme with named pipe IPC for external activation ## Installation ### Prerequisites ```powershell # Windows 10 20H2+ or Windows 11 # .NET 10.0 SDK winget install Microsoft.DotNet.SDK.10 # Windows 10 SDK (for WinUI build) - via Visual Studio or standalone # WebView2 Runtime (pre-installed on modern Windows) ``` ### Build ```powershell # Check prerequisites .\build.ps1 -CheckOnly # Build all projects .\build.ps1 # Build specific project .\build.ps1 -Project WinUI # Build for ARM64 dotnet build src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj -r win-arm64 # Build MSIX package (for camera/mic consent prompts) dotnet build src/OpenClaw.Tray.WinUI -r win-x64 -p:PackageMsix=true ``` ### Run Tray App ```powershell # Build and launch the tray app .\run-app-local.ps1 # Run without rebuilding .\run-app-local.ps1 -NoBuild # Run isolated (separate settings file for testing) .\run-app-local.ps1 -Isolated # Test alpha updates from Release build .\run-app-local.ps1 -Configuration Release -Isolated -UpdateChannel alpha ``` ### Run CLI ```powershell # Show help dotnet run --project src/OpenClaw.Cli -- --help # Send message using tray settings dotnet run --project src/OpenClaw.Cli -- --message "test message" # Loop sends with probing dotnet run --project src/OpenClaw.Cli -- --repeat 5 --delay-ms 1000 --probe-read --verbose # Override gateway URL/token dotnet run --project src/OpenClaw.Cli -- --url ws://127.0.0.1:18789 --token "$env:OPENCLAW_TOKEN" --message "test" ``` ## Project Structure ``` openclaw-windows-node/ ├── src/ │ ├── OpenClaw.Tray.WinUI/ # System tray app (WinUI 3) │ │ ├── Services/ # Gateway, Notification, Update, Node │ │ ├── ViewModels/ # MVVM view models │ │ ├── Views/ # XAML pages and windows │ │ ├── Capabilities/ # Node capability implementations │ │ └── Models/ # Data models │ ├── OpenClaw.Shared/ # Gateway client library │ │ ├── GatewayClient.cs # WebSocket client │ │ ├── Models/ # DTOs for gateway API │ │ └── Extensions/ # Helper methods │ └── OpenClaw.Cli/ # CLI validator └── docs/ # Documentation ``` ## Key Components ### GatewayClient (Shared Library) The core WebSocket client for OpenClaw gateway integration. ```csharp using OpenClaw.Shared; using OpenClaw.Shared.Models; // Create client var client = new GatewayClient("ws://127.0.0.1:18789", "your-token-here"); // Handle events client.MessageReceived += (s, msg) => { Console.WriteLine($"Topic: {msg.Topic}, Data: {msg.Data}"); }; client.ConnectionStateChanged += (s, state) => { Console.WriteLine($"Connection: {state}"); }; // Connect await client.ConnectAsync(); // Subscribe to topics await client.SubscribeAsync(new[] { "gateway.sessions", "gateway.usage", "gateway.channels" }); // Send RPC request var response = await client.SendRequestAsync<ChatSendResponse>( "chat.send", new { message = "Hello from Windows!", sessionId = "session-123" } ); // Unsubscribe await client.UnsubscribeAsync(new[] { "gateway.sessions" }); // Disconnect await client.DisconnectAsync(); ``` ### Node Registration and Capabilities When Node Mode is enabled, the tray app registers as a node and handles capability requests. ```csharp // In NodeService.cs public class NodeService { private readonly GatewayClient _gatewayClient; private readonly Dictionary<string, INodeCapability> _capabilities; public async Task RegisterNodeAsync() { // Register capabilities var capabilities = new[] { "system.notify", "system.run", "canvas.present", "screen.snapshot", "camera.snap", "stt.transcribe", "tts.speak", "location.get", "device.info" }; var request = new { deviceId = GetDeviceId(), capabilities = capabilities, metadata = new { platform = "windows", version = GetAppVersion(), hostname = Environment.MachineName } }; await _gatewayClient.SendRequestAsync<object>("node.register", request); } public async Task HandleNodeRequestAsync(NodeRequest request) { if (!_capabilities.TryGetValue(request.Method, out var capability)) { throw new NotSupportedException($"Capability not supported: {request.Method}"); } var result = await capability.ExecuteAsync(request.Params); await _gatewayClient.SendRequestAsync<object>("node.response", new { requestId = request.Id, result = result }); } } ``` ### Implementing a Node Capability ```csharp // In Capabilities/SystemNotifyCapability.cs using Windows.UI.Notifications; using Microsoft.Toolkit.Uwp.Notifications; public class SystemNotifyCapability : INodeCapability { public string Method => "system.notify"; public async Task<object> ExecuteAsync(JsonElement? parameters) { var title = parameters?.GetProperty("title").GetString() ?? "Notification"; var message = parameters?.GetProperty("message").GetString() ?? ""; var priority = parameters?.GetProperty("priority").GetString() ?? "normal"; // Create toast notification new ToastContentBuilder() .AddText(title) .AddText(message) .AddArgument("action", "viewConversation") .AddArgument("conversationId", Guid.NewGuid().ToString()) .Show(); return new { success = true }; } } ``` ### Screen Snapshot Capability ```csharp // In Capabilities/ScreenSnapshotCapability.cs using System.Drawing; using System.Drawing.Imaging; using Windows.Graphics.Capture; public class ScreenSnapshotCapability : INodeCapability { public string Method => "screen.snapshot"; public async Task<object> ExecuteAsync(JsonElement? parameters) { var displayIndex = parameters?.GetProperty("display").GetInt32() ?? 0; var format = parameters?.GetProperty("format").GetString() ?? "png"; // Capture screen using Windows.Graphics.Capture var bounds = Screen.AllScreens[displayIndex].Bounds; using var bitmap = new Bitmap(bounds.Width, bounds.Height); using var graphics = Graphics.FromImage(bitmap); graphics.CopyFromScreen( bounds.Location, Point.Empty, bounds.Size ); // Convert to base64 using var ms = new MemoryStream(); var imageFormat = format.ToLower() switch { "jpg" => ImageFormat.Jpeg, "jpeg" => ImageFormat.Jpeg, _ => ImageFormat.Png }; bitmap.Save(ms, imageFormat); var base64 = Convert.ToBase64String(ms.ToArray()); return new { data = base64, format = format, width = bounds.Width, height = bounds.Height }; } } ``` ### Camera Capability ```csharp // In Capabilities/CameraCapability.cs using Windows.Media.Capture; using Windows.Media.MediaProperties; using Windows.Storage; public class CameraSnapCapability : INodeCapability { public string Method => "camera.snap"; public async Task<object> ExecuteAsync(JsonElement? parameters) { var deviceId = parameters?.GetProperty("deviceId").GetString(); var format = parameters?.GetProperty("format").GetString() ?? "jpg"; // Initialize MediaCapture var capture = new MediaCapture(); var settings = new MediaCaptureInitializationSettings(); if (!string.IsNullOrEmpty(deviceId)) { settings.VideoDeviceId = deviceId; } await capture.InitializeAsync(settings); // Capture photo var file = await ApplicationData.Current.TemporaryFolder .CreateFileAsync($"snap_{Guid.NewGuid()}.{format}"); var imageEncoding = format.ToLower() == "png" ? ImageEncodingProperties.CreatePng() : ImageEncodingProperties.CreateJpeg(); await capture.CapturePhotoToStorageFileAsync(imageEncoding, file); // Read and encode var buffer = await FileIO.ReadBufferAsync(file); var bytes = buffer.ToArray(); var base64 = Convert.ToBase64String(bytes); // Cleanup await file.DeleteAsync(); capture.Dispose(); return new { data = base64, format = format }; } } ``` ### STT (Speech-to-Text) Capability ```csharp // In Capabilities/SttTranscribeCapability.cs using Windows.Media.Capture; using Windows.Media.SpeechRecognition; public class SttTranscribeCapability : INodeCapability { public string Method => "stt.transcribe"; public async Task<object> ExecuteAsync(JsonElement? parameters) { var durationSec = parameters?.GetProperty("durationSec").GetDouble() ?? 5.0; var language = parameters?.GetProperty("language").GetString() ?? "en-US"; // Check if STT is enabled in settings if (!SettingsService.Current.IsSttEnabled) { throw new InvalidOperationException("STT is disabled in settings"); } // Initialize speech recognizer var recognizer = new SpeechRecognizer(new Windows.Globalization.Language(language)); await recognizer.CompileConstraintsAsync(); // Start continuous recognition var tcs = new TaskCompletionSource<string>(); var recognizedText = new StringBuilder(); recognizer.ContinuousRecognitionSession.ResultGenerated += (s, e) => { if (e.Result.Status == SpeechRecognitionResultStatus.Success) { recognizedText.AppendLine(e.Result.Text); } }; await recognizer.ContinuousRecognitionSession.StartAsync(); await Task.Delay(TimeSpan.FromSeconds(durationSec)); await recognizer.ContinuousRecognitionSession.StopAsync(); recognizer.Dispose(); return new { text = recognizedText.ToString().Trim(), durationSec = durationSec, language = language }; } } ``` ### TTS (Text-to-Speech) Capability ```csharp // In Capabilities/TtsSpeakCapability.cs using Windows.Media.SpeechSynthesis; using Windows.Media.Playback; public class TtsSpeakCapability : INodeCapability { public string Method => "tts.speak"; public async Task<object> ExecuteAsync(JsonElement? parameters) { var text = parameters?.GetProperty("text").GetString() ?? throw new ArgumentException("text is required"); var voice = parameters?.GetProperty("voice").GetString(); var provider = parameters?.GetProperty("provider").GetString() ?? "system"; if (provider == "elevenlabs") { return await SpeakWithElevenLabsAsync(text, voice); } // Use Windows speech synthesis var synthesizer = new SpeechSynthesizer(); if (!string.IsNullOrEmpty(voice)) { var selectedVoice = SpeechSynthesizer.AllVoices .FirstOrDefault(v => v.DisplayName == voice); if (selectedVoice != null) { synthesizer.Voice = selectedVoice; } } var stream = await synthesizer.SynthesizeTextToStreamAsync(text); var player = new MediaPlayer(); player.Source = Windows.Media.Core.MediaSource.CreateFromStream( stream, stream.ContentType ); player.Play(); // Wait for playback to complete var tcs = new TaskCompletionSource<bool>(); player.MediaEnded += (s, e) => tcs.SetResult(true); await tcs.Task; return new { success = true }; } private async Task<object> SpeakWithElevenLabsAsync(string text, string voice) { var apiKey = Environment.GetEnvironmentVariable("ELEVENLABS_API_KEY") ?? throw new InvalidOperationException("ELEVENLABS_API_KEY not set"); var voiceId = voice ?? "21m00Tcm4TlvDq8ikWAM"; // Default voice using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("xi-api-key", apiKey); var request = new { text = text, model_id = "eleven_monolingual_v1", voice_settings = new { stability = 0.5, similarity_boost = 0.75 } }; var response = await httpClient.PostAsJsonAsync(
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen