Skip to content
Closed
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
49 changes: 40 additions & 9 deletions src/OpenClaw.Shared/Capabilities/CameraCapability.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OpenClaw.Shared.Capabilities;
Expand Down Expand Up @@ -32,19 +33,27 @@ public CameraCapability(IOpenClawLogger logger) : base(logger)
private static int Clamp(int value, int min, int max)
=> value < min ? min : (value > max ? max : value);

public override async Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest request)
public override Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest request)
=> ExecuteAsync(request, CancellationToken.None);

public override async Task<NodeInvokeResponse> ExecuteAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
return request.Command switch
{
"camera.list" => await HandleListAsync(request),
"camera.snap" => await HandleSnapAsync(request),
"camera.clip" => await HandleClipAsync(request),
"camera.list" => await HandleListAsync(request, cancellationToken),
"camera.snap" => await HandleSnapAsync(request, cancellationToken),
"camera.clip" => await HandleClipAsync(request, cancellationToken),
_ => Error($"Unknown command: {request.Command}")
};
}

private async Task<NodeInvokeResponse> HandleListAsync(NodeInvokeRequest request)
private async Task<NodeInvokeResponse> HandleListAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Logger.Info("camera.list");

if (ListRequested == null)
Expand All @@ -57,6 +66,10 @@ private async Task<NodeInvokeResponse> HandleListAsync(NodeInvokeRequest request
var cameras = await ListRequested();
return Success(new { cameras });
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
Logger.Error("Camera list failed", ex);
Expand All @@ -71,8 +84,11 @@ private async Task<NodeInvokeResponse> HandleListAsync(NodeInvokeRequest request
private const int MaxQuality = 100;
private const int MaxClipDurationMs = 60_000;

private async Task<NodeInvokeResponse> HandleSnapAsync(NodeInvokeRequest request)
private async Task<NodeInvokeResponse> HandleSnapAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var deviceId = GetStringArg(request.Args, "deviceId");
var format = GetStringArg(request.Args, "format", "jpeg");
var maxWidth = Clamp(GetIntArg(request.Args, "maxWidth", 1280), MinCameraDimension, MaxCameraWidth);
Expand All @@ -92,7 +108,8 @@ private async Task<NodeInvokeResponse> HandleSnapAsync(NodeInvokeRequest request
DeviceId = deviceId,
Format = format ?? "jpeg",
MaxWidth = maxWidth,
Quality = quality
Quality = quality,
CancellationToken = cancellationToken
});

return Success(new
Expand All @@ -103,15 +120,22 @@ private async Task<NodeInvokeResponse> HandleSnapAsync(NodeInvokeRequest request
base64 = result.Base64
});
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
Logger.Error("Camera snap failed", ex);
return Error("Snap failed");
}
}

private async Task<NodeInvokeResponse> HandleClipAsync(NodeInvokeRequest request)
private async Task<NodeInvokeResponse> HandleClipAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var deviceId = GetStringArg(request.Args, "deviceId");
// Floor at 100ms — anything shorter is meaningless and a 0/negative
// value previously slipped through the `Math.Min` cap.
Expand All @@ -133,7 +157,8 @@ private async Task<NodeInvokeResponse> HandleClipAsync(NodeInvokeRequest request
DeviceId = deviceId,
DurationMs = durationMs,
IncludeAudio = includeAudio,
Format = format
Format = format,
CancellationToken = cancellationToken
});

return Success(new
Expand All @@ -144,6 +169,10 @@ private async Task<NodeInvokeResponse> HandleClipAsync(NodeInvokeRequest request
hasAudio = result.HasAudio
});
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
Logger.Error("Camera clip failed", ex);
Expand All @@ -165,6 +194,7 @@ public class CameraSnapArgs
public string Format { get; set; } = "jpeg";
public int MaxWidth { get; set; } = 1280;
public int Quality { get; set; } = 80;
public CancellationToken CancellationToken { get; set; }
}

public class CameraSnapResult
Expand All @@ -181,6 +211,7 @@ public class CameraClipArgs
public int DurationMs { get; set; } = 3000;
public bool IncludeAudio { get; set; } = true;
public string Format { get; set; } = "mp4";
public CancellationToken CancellationToken { get; set; }
}

public class CameraClipResult
Expand Down
39 changes: 31 additions & 8 deletions src/OpenClaw.Shared/Capabilities/ScreenCapability.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OpenClaw.Shared.Capabilities;
Expand Down Expand Up @@ -27,12 +28,17 @@ public ScreenCapability(IOpenClawLogger logger) : base(logger)
{
}

public override async Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest request)
public override Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest request)
=> ExecuteAsync(request, CancellationToken.None);

public override async Task<NodeInvokeResponse> ExecuteAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
return request.Command switch
{
"screen.snapshot" => await HandleCaptureAsync(request),
"screen.record" => await HandleRecordAsync(request),
"screen.snapshot" => await HandleCaptureAsync(request, cancellationToken),
"screen.record" => await HandleRecordAsync(request, cancellationToken),
_ => Error($"Unknown command: {request.Command}")
};
}
Expand All @@ -44,8 +50,11 @@ public override async Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest re
private const int MaxQuality = 100;
private const int MaxScreenIndex = 32; // far above any plausible monitor count

private async Task<NodeInvokeResponse> HandleCaptureAsync(NodeInvokeRequest request)
private async Task<NodeInvokeResponse> HandleCaptureAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var format = GetStringArg(request.Args, "format", "png");
var maxWidth = Clamp(GetIntArg(request.Args, "maxWidth", 1920), MinDimension, MaxScreenWidth);
var quality = Clamp(GetIntArg(request.Args, "quality", 80), MinQuality, MaxQuality);
Expand All @@ -68,7 +77,8 @@ private async Task<NodeInvokeResponse> HandleCaptureAsync(NodeInvokeRequest requ
MaxWidth = maxWidth,
Quality = quality,
MonitorIndex = screenIndex,
IncludePointer = includePointer
IncludePointer = includePointer,
CancellationToken = cancellationToken
});

var image = $"data:image/{result.Format.ToLowerInvariant()};base64,{result.Base64}";
Expand All @@ -81,15 +91,22 @@ private async Task<NodeInvokeResponse> HandleCaptureAsync(NodeInvokeRequest requ
image
});
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
Logger.Error("Screen capture failed", ex);
return Error("Capture failed");
}
}

private async Task<NodeInvokeResponse> HandleRecordAsync(NodeInvokeRequest request)
private async Task<NodeInvokeResponse> HandleRecordAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var format = GetStringArg(request.Args, "format", "mp4");
if (!string.IsNullOrWhiteSpace(format) &&
!string.Equals(format, "mp4", StringComparison.OrdinalIgnoreCase))
Expand Down Expand Up @@ -118,7 +135,8 @@ private async Task<NodeInvokeResponse> HandleRecordAsync(NodeInvokeRequest reque
Fps = fps,
ScreenIndex = screenIndex,
Format = "mp4",
IncludeAudio = includeAudio
IncludeAudio = includeAudio,
CancellationToken = cancellationToken
});

return Success(new
Expand All @@ -131,6 +149,10 @@ private async Task<NodeInvokeResponse> HandleRecordAsync(NodeInvokeRequest reque
hasAudio = result.HasAudio
});
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
Logger.Error("Screen recording failed", ex);
Expand Down Expand Up @@ -166,6 +188,7 @@ public class ScreenCaptureArgs
public int Quality { get; set; } = 80;
public int MonitorIndex { get; set; } = 0;
public bool IncludePointer { get; set; } = true;
public CancellationToken CancellationToken { get; set; }
}

public class ScreenCaptureResult
Expand All @@ -183,6 +206,7 @@ public class ScreenRecordArgs
public double Fps { get; set; } = 10;
public int ScreenIndex { get; set; }
public bool IncludeAudio { get; set; }
public CancellationToken CancellationToken { get; set; }
}

public class ScreenRecordResult
Expand All @@ -196,4 +220,3 @@ public class ScreenRecordResult
public int Height { get; set; }
public bool HasAudio { get; set; }
}

8 changes: 6 additions & 2 deletions src/OpenClaw.Shared/Capabilities/SttCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private async Task<NodeInvokeResponse> HandleTranscribeAsync(
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("Transcribe canceled");
return Error("cancelled");
}
catch (Exception ex)
{
Expand Down Expand Up @@ -237,7 +237,7 @@ private async Task<NodeInvokeResponse> HandleListenAsync(
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("Listen canceled");
return Error("cancelled");
}
catch (Exception ex)
{
Expand All @@ -264,6 +264,10 @@ private async Task<NodeInvokeResponse> HandleStatusAsync(CancellationToken cance
isBoundedTranscribeSupported = result.IsBoundedTranscribeSupported
});
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
// Status must not leak engine internals; carry only a fixed message.
Expand Down
22 changes: 17 additions & 5 deletions src/OpenClaw.Shared/Capabilities/SystemCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,16 @@ public void SetV2Handler(IExecApprovalV2Handler handler)
}

public override async Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest request)
=> await ExecuteAsync(request, CancellationToken.None);

public override async Task<NodeInvokeResponse> ExecuteAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
return request.Command switch
{
"system.notify" => await HandleNotifyAsync(request),
"system.run" => await HandleRunAsync(request),
"system.run" => await HandleRunAsync(request, cancellationToken),
"system.run.prepare" => HandleRunPrepare(request),
"system.which" => HandleWhich(request),
"system.execApprovals.get" => HandleExecApprovalsGet(),
Expand Down Expand Up @@ -254,9 +259,12 @@ private NodeInvokeResponse HandleRunPrepare(NodeInvokeRequest request)
});
}

private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
private async Task<NodeInvokeResponse> HandleRunAsync(
NodeInvokeRequest request,
CancellationToken cancellationToken)
{
var correlationId = Guid.NewGuid().ToString("N")[..8];
cancellationToken.ThrowIfCancellationRequested();

// Routing seam (rail 2): select path, delegate — no approval logic here.
if (_v2Handler != null)
Expand Down Expand Up @@ -365,7 +373,7 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
if (_approvalPolicy != null)
{
var approval = _approvalPolicy.Evaluate(fullCommand, shell);
if (!await EnsureApprovedAsync(fullCommand, shell, approval))
if (!await EnsureApprovedAsync(fullCommand, shell, approval, cancellationToken))
{
Logger.Warn($"system.run DENIED: {fullCommand} ({approval.Reason})");
return Error($"Command denied by exec policy: {approval.Reason}");
Expand All @@ -381,7 +389,7 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
foreach (var target in parseResult.Targets)
{
var innerApproval = _approvalPolicy.Evaluate(target.Command, target.Shell);
if (!await EnsureApprovedAsync(target.Command, target.Shell, innerApproval))
if (!await EnsureApprovedAsync(target.Command, target.Shell, innerApproval, cancellationToken))
{
Logger.Warn($"system.run DENIED: {target.Command} ({innerApproval.Reason})");
return Error($"Command denied by exec policy: {innerApproval.Reason}");
Expand All @@ -399,7 +407,7 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
Cwd = cwd,
TimeoutMs = timeoutMs,
Env = env
});
}, cancellationToken);

return Success(new
{
Expand All @@ -410,6 +418,10 @@ private async Task<NodeInvokeResponse> HandleRunAsync(NodeInvokeRequest request)
durationMs = result.DurationMs
});
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("cancelled");
}
catch (Exception ex)
{
Logger.Error("system.run failed", ex);
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Shared/Capabilities/TtsCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public override async Task<NodeInvokeResponse> ExecuteAsync(
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Error("Speak canceled");
return Error("cancelled");
}
catch (Exception ex)
{
Expand Down
Loading