diff --git a/src/OpenClaw.Shared/Capabilities/DeviceCapability.cs b/src/OpenClaw.Shared/Capabilities/DeviceCapability.cs
index 4a01ec8..e975f6c 100644
--- a/src/OpenClaw.Shared/Capabilities/DeviceCapability.cs
+++ b/src/OpenClaw.Shared/Capabilities/DeviceCapability.cs
@@ -11,7 +11,9 @@ using System.Threading.Tasks;
namespace OpenClaw.Shared.Capabilities;
///
-/// Device metadata and lightweight health/status capability.
+/// Device metadata and system health/status capability.
+/// device.info - static device metadata (no provider needed).
+/// device.status - rich system health data via injected IDeviceStatusProvider.
///
public class DeviceCapability : NodeCapabilityBase
{
@@ -23,20 +25,28 @@ public class DeviceCapability : NodeCapabilityBase
"device.status"
];
+ private static readonly HashSet _validSections = new(
+ ["os", "cpu", "memory", "disk", "battery"],
+ StringComparer.OrdinalIgnoreCase);
+
+ private readonly IDeviceStatusProvider? _provider;
+
public override IReadOnlyList Commands => _commands;
- public DeviceCapability(IOpenClawLogger logger) : base(logger)
+ public DeviceCapability(IOpenClawLogger logger, IDeviceStatusProvider provider)
+ : base(logger)
{
+ _provider = provider;
}
- public override Task ExecuteAsync(NodeInvokeRequest request)
+ public override async Task ExecuteAsync(NodeInvokeRequest request)
{
- return Task.FromResult(request.Command switch
+ return request.Command switch
{
"device.info" => HandleInfo(),
- "device.status" => HandleStatus(),
+ "device.status" => await HandleStatusAsync(request),
_ => Error($"Unknown command: {request.Command}")
- });
+ };
}
private NodeInvokeResponse HandleInfo()
@@ -60,29 +70,133 @@ public class DeviceCapability : NodeCapabilityBase
});
}
- private NodeInvokeResponse HandleStatus()
+ private async Task HandleStatusAsync(NodeInvokeRequest request)
{
- Logger.Info("device.status");
+ if (_provider == null)
+ return Error("Device status provider not available");
- var storage = GetStorageStatus(Logger);
- var network = GetNetworkStatus(Logger);
+ var sections = GetStringArrayArg(request.Args, "sections");
- return Success(new
+ // Reject unknown section names
+ var invalid = sections.Where(s => !_validSections.Contains(s)).ToArray();
+ if (invalid.Length > 0)
{
- battery = new
+ return Error($"Unknown sections: {string.Join(", ", invalid)}. "
+ + $"Valid: {string.Join(", ", _validSections)}");
+ }
+
+ bool all = sections.Length == 0;
+ var result = new Dictionary
+ {
+ ["collectedAt"] = DateTime.UtcNow.ToString("o")
+ };
+
+ if (all || sections.Contains("os", StringComparer.OrdinalIgnoreCase))
+ result["os"] = SafeCollect("os", () => _provider.GetOsInfo());
+
+ if (all || sections.Contains("cpu", StringComparer.OrdinalIgnoreCase))
+ result["cpu"] = await SafeCollectAsync("cpu", () => _provider.GetCpuInfoAsync());
+
+ if (all || sections.Contains("memory", StringComparer.OrdinalIgnoreCase))
+ result["memory"] = SafeCollect("memory", () => _provider.GetMemoryInfo());
+
+ if (all || sections.Contains("disk", StringComparer.OrdinalIgnoreCase))
+ result["disk"] = SafeCollect("disk", () => _provider.GetDiskInfo());
+
+ if (all || sections.Contains("battery", StringComparer.OrdinalIgnoreCase))
+ result["battery"] = SafeCollect("battery", () => WrapBatteryWithLegacyFields(_provider.GetBatteryInfo()));
+
+ // Always ensure legacy battery fields exist for backward compatibility.
+ // Old contract: { level: null, state: "unknown", lowPowerModeEnabled: false }
+ // Covers: battery not requested (filtered out), provider threw (SafeCollect
+ // returned { error }), or battery is null.
+ {
+ var hasBattery = result.TryGetValue("battery", out var batteryVal) && batteryVal != null;
+ var isError = hasBattery && batteryVal!.GetType().GetProperty("error") != null;
+
+ if (!hasBattery || isError)
{
- level = (double?)null,
- state = "unknown",
- lowPowerModeEnabled = false
- },
- thermal = new
- {
- state = "nominal"
- },
- storage,
- network,
- uptimeSeconds = Environment.TickCount64 / 1000.0
- });
+ string? errorMsg = null;
+ if (isError)
+ {
+ var errProp = batteryVal!.GetType().GetProperty("error")!.GetValue(batteryVal);
+ errorMsg = errProp?.ToString();
+ }
+
+ result["battery"] = new
+ {
+ level = (double?)null,
+ state = "unknown",
+ lowPowerModeEnabled = false,
+ error = errorMsg
+ };
+ }
+ }
+
+ // Legacy fields preserved for backward compatibility with existing consumers.
+ result["thermal"] = new { state = "nominal" };
+ result["storage"] = SafeCollect("storage", () => GetStorageStatus());
+ result["network"] = SafeCollect("network", () => GetNetworkStatus());
+ result["uptimeSeconds"] = Environment.TickCount64 / 1000.0;
+
+ return Success(result);
+ }
+
+ /// Per-section fault tolerance: one section failing doesn't kill the whole response.
+ private object? SafeCollect(string section, Func