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

Ir para a instalação

Informações da origem

Repositório
reason-machines/hermes-skills
Última atividade na origem
6 de junho de 2026 às 14:34
Idioma detectado do SKILL.md
inglês
Estrelas
5
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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(
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub