Sequences are now implemented in UE Wingman

This commit is contained in:
2026-04-17 06:46:18 -04:00
parent f19e8ccb72
commit d396f394ab
3 changed files with 113 additions and 30 deletions

View File

@@ -0,0 +1,40 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "Sequence.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Sequence : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description=
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
FWingJsonArray Subcommands;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Execute multiple commands in one request. Each subcommand produces its own content block in the response. "
"Nested Sequence commands are not allowed."));
}
virtual void Handle() override
{
// The actual code that implements Sequence is hardwired into
// WingServer. Because of that, this handler is never actually called
// under normal conditions. The handler exists for two reasons: to
// provide documentation, and also to catch the case where somebody
// nests a sequence inside another sequence (WingServer doesn't catch
// that).
//
WingOut::Stdout.Print(TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
}
};

View File

@@ -170,6 +170,70 @@ TStatId UWingServer::GetStatId() const
// ============================================================ // ============================================================
FString UWingServer::HandleRequest(const FString& Line) FString UWingServer::HandleRequest(const FString& Line)
{
// Parse the request as JSON before doing anything else.
TSharedPtr<FJsonValue> Value;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
if (!FJsonSerializer::Deserialize(Reader, Value))
return PackageResponses({TEXT("Invalid Json")});
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
if (!Value->TryGetObject(RequestPtr))
return PackageResponses({TEXT("Json must be an object")});
TSharedPtr<FJsonObject> Request = *RequestPtr;
FString Command;
Request->TryGetStringField(TEXT("command"), Command);
if (Command == TEXT("Sequence"))
{
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
TArray<FString> Responses;
Responses.Reserve(Subcommands->Num());
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
{
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
if (!Sub->TryGetObject(SubObjPtr))
Responses.Add(TEXT("Subcommand must be a JSON object."));
else
Responses.Add(HandleJsonRequest(*SubObjPtr));
}
return PackageResponses(Responses);
}
return PackageResponses({HandleJsonRequest(Request)});
}
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
{
TArray<TSharedPtr<FJsonValue>> Blocks;
Blocks.Reserve(Responses.Num());
for (const FString& Response : Responses)
{
// Unreal's JSON writer terminates string serialization at the first
// embedded null byte rather than escaping it, which would silently
// truncate output. Sanitize null bytes to spaces.
FString Sanitized = Response;
for (int32 i = 0; i < Sanitized.Len(); ++i)
{
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
}
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
Block->SetStringField(TEXT("type"), TEXT("text"));
Block->SetStringField(TEXT("text"), Sanitized);
Blocks.Add(MakeShared<FJsonValueObject>(Block));
}
FString OutJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
FJsonSerializer::Serialize(Blocks, Writer);
return OutJson;
}
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
{ {
LogCapture.CapturedErrors.Empty(); LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true; LogCapture.bEnabled = true;
@@ -178,7 +242,7 @@ FString UWingServer::HandleRequest(const FString& Line)
bSuggestHandlerHelp = false; bSuggestHandlerHelp = false;
LastHandler = nullptr; LastHandler = nullptr;
TryCallHandler(Line); TryCallHandler(Request);
Notifier.SendNotifications(); Notifier.SendNotifications();
LogCapture.bEnabled = false; LogCapture.bEnabled = false;
@@ -202,37 +266,11 @@ FString UWingServer::HandleRequest(const FString& Line)
} }
FString Result = WingOut::StdoutBuffer.ToString(); FString Result = WingOut::StdoutBuffer.ToString();
WingOut::StdoutBuffer.Reset(); WingOut::StdoutBuffer.Reset();
for (int32 i = 0; i < Result.Len(); ++i) return Result;
{
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
} }
// Wrap the text in an MCP content-block array: [{"type":"text","text":"..."}] void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
Block->SetStringField(TEXT("type"), TEXT("text"));
Block->SetStringField(TEXT("text"), Result);
TArray<TSharedPtr<FJsonValue>> Blocks;
Blocks.Add(MakeShared<FJsonValueObject>(Block));
FString OutJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
FJsonSerializer::Serialize(Blocks, Writer);
return OutJson;
}
void UWingServer::TryCallHandler(const FString &Line)
{ {
// Turn the request string into a JSON tree.
TSharedPtr<FJsonObject> Request;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
FJsonSerializer::Deserialize(Reader, Request);
if (!Request.IsValid())
{
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
return;
}
// Extract the command from the request. // Extract the command from the request.
FString Command; FString Command;
if (!Request->TryGetStringField(TEXT("command"), Command)) if (!Request->TryGetStringField(TEXT("command"), Command))

View File

@@ -11,6 +11,7 @@
#include "WingServer.generated.h" #include "WingServer.generated.h"
class FSocket; class FSocket;
class FJsonObject;
/** /**
* UWingServer — editor subsystem that listens on a TCP socket and dispatches * UWingServer — editor subsystem that listens on a TCP socket and dispatches
@@ -61,6 +62,9 @@ public:
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation); static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; } static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
/** Package a list of response texts into a single serialized JSON content-block array. */
static FString PackageResponses(const TArray<FString>& Responses);
private: private:
static UWingServer* GWingServer; static UWingServer* GWingServer;
@@ -78,7 +82,8 @@ private:
// Handle a complete JSON line and return the response JSON // Handle a complete JSON line and return the response JSON
FString HandleRequest(const FString& Line); FString HandleRequest(const FString& Line);
void TryCallHandler(const FString &Line); FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
void TryCallHandler(TSharedPtr<FJsonObject> Request);
// ----- TCP server ----- // ----- TCP server -----
FSocket* ListenSocket = nullptr; FSocket* ListenSocket = nullptr;