feat(CUB-19): implement AgentStatus SignalR hub for real-time updates
- Add AgentStatusHub with typed IAgentStatusClient interface
- Hub at /hubs/agent-status (matches design spec)
- Fleet group + per-agent group subscription
- AgentStatusChanged and AgentTaskProgress push events
- Extension methods for server-side push via IHubContext
- Add GatewayEventBridgeService background service
- Connects to OpenClaw Gateway WebSocket (v3 protocol)
- Handles challenge → connect → hello-ok handshake
- Bridges sessions.changed, session.message, session.tool events
- Translates Gateway session status to AgentStatus enum
- Maintains in-memory fleet state for snapshot queries
- Add REST API controllers
- GET /api/agents — fleet status snapshot
- GET /api/agents/{agentId} — single agent status
- GET /api/logs/{agentId} — agent session logs (stub)
- POST /api/command/stop/{agentId} — stop agent
- POST /api/command/restart/{agentId} — restart agent
- POST /api/command/steer/{agentId} — inject message
- Add models matching TypeScript spec interfaces
- AgentStatusUpdate, TaskProgressUpdate, AgentCardData
- AgentStatus enum (active/idle/thinking/error)
- Configure CORS with credentials for SignalR WebSocket
- Configure Swagger/OpenAPI with XML doc comments
- Agent role map matching frontend AGENT_ROLES constant
This commit is contained in:
122
backend/ControlCenter/Controllers/CommandController.cs
Normal file
122
backend/ControlCenter/Controllers/CommandController.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ControlCenter.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// REST API for sending control commands to agents.
|
||||
/// Provides the Command Hub's action endpoints for agent lifecycle control.
|
||||
///
|
||||
/// <para>API contract for Rex (Frontend):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>POST /api/command/stop/{agentId}</c> — Stop/abort an agent's active session</item>
|
||||
/// <item><c>POST /api/command/restart/{agentId}</c> — Restart an agent</item>
|
||||
/// <item><c>POST /api/command/steer/{agentId}</c> — Inject a message into an agent's session</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>Commands are forwarded to the OpenClaw Gateway via the
|
||||
/// WebSocket bridge service. The Gateway handles the actual execution.</para>
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CommandController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<CommandController> _logger;
|
||||
|
||||
public CommandController(ILogger<CommandController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops (aborts) an agent's active session.
|
||||
/// Sends an abort command to the OpenClaw Gateway.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The agent identifier to stop.</param>
|
||||
/// <returns>Confirmation of the stop command.</returns>
|
||||
/// <response code="200">Stop command sent successfully.</response>
|
||||
/// <response code="404">No active session found for the agent.</response>
|
||||
[HttpPost("stop/{agentId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public IActionResult StopAgent(string agentId)
|
||||
{
|
||||
_logger.LogInformation("Stop command received for agent {AgentId}", agentId);
|
||||
|
||||
// TODO: Forward to Gateway via bridge service
|
||||
// await _bridgeService.SendRpcAsync("sessions.abort", new { sessionKey = ... });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
agentId,
|
||||
command = "stop",
|
||||
status = "sent",
|
||||
timestamp = DateTime.UtcNow.ToString("o")
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts an agent by aborting the current session and allowing
|
||||
/// a new one to start on the next incoming message.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The agent identifier to restart.</param>
|
||||
/// <returns>Confirmation of the restart command.</returns>
|
||||
/// <response code="200">Restart command sent successfully.</response>
|
||||
[HttpPost("restart/{agentId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IActionResult RestartAgent(string agentId)
|
||||
{
|
||||
_logger.LogInformation("Restart command received for agent {AgentId}", agentId);
|
||||
|
||||
// TODO: Forward to Gateway — abort current session + signal restart
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
agentId,
|
||||
command = "restart",
|
||||
status = "sent",
|
||||
timestamp = DateTime.UtcNow.ToString("o")
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Steers (injects a message into) an agent's active session.
|
||||
/// Used by operators to redirect an agent's task mid-execution.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The agent identifier to steer.</param>
|
||||
/// <param name="request">The steering message to inject.</param>
|
||||
/// <returns>Confirmation of the steer command.</returns>
|
||||
/// <response code="200">Steer command sent successfully.</response>
|
||||
/// <response code="400">Missing or empty message.</response>
|
||||
[HttpPost("steer/{agentId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public IActionResult SteerAgent(string agentId, [FromBody] SteerRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Message))
|
||||
{
|
||||
return BadRequest(new { error = "Message is required" });
|
||||
}
|
||||
|
||||
_logger.LogInformation("Steer command received for agent {AgentId}: {Message}",
|
||||
agentId, request.Message.Length > 100
|
||||
? request.Message[..100] + "..." : request.Message);
|
||||
|
||||
// TODO: Forward to Gateway via bridge service
|
||||
// await _bridgeService.SendRpcAsync("sessions.steer", new { sessionKey = ..., message = request.Message });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
agentId,
|
||||
command = "steer",
|
||||
message = request.Message,
|
||||
status = "sent",
|
||||
timestamp = DateTime.UtcNow.ToString("o")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request body for the steer command.
|
||||
/// </summary>
|
||||
/// <param name="Message">The message to inject into the agent's session.</param>
|
||||
public record SteerRequest(string Message);
|
||||
Reference in New Issue
Block a user