Zum Hauptinhalt springen

Controller-Implementierung

Dieser Leitfaden erklärt, wie Sie einen benutzerdefinierten Controller-Client für das Otto-Relay-Protokoll implementieren. Nach Abschluss dieses Leitfadens kann Ihr Controller authentifizieren, Befehle an Nodes weiterleiten, Stream-Sessions behandeln und den Token-Lebenszyklus korrekt verwalten.

Bevor Sie beginnen

  • Lesen Sie Architektur, um Systemrollen und Befehlslebenszyklus zu verstehen.
  • Lesen Sie Kopplung und Auth, um den Token-Ablauf zu verstehen.
  • Stellen Sie sicher, dass ein Relay läuft (otto start), gegen das Sie testen können.

Erforderliche Fähigkeiten

Ihr Controller muss folgendes beherrschen:

  1. Token-Lebenszyklus — Client registrieren, Anmeldeinformationen austauschen, Token vor Ablauf aktualisieren.
  2. WebSocket-Auth-Sequenzierunghelloauth → auf auth_ack warten, bevor Befehle gesendet werden.
  3. Anfragekorrelation — eindeutige requestId pro Envelope verwenden; Antworten nach dieser ID korrelieren.
  4. Deterministische Stream-Beendigung — explizit deabonnieren; command_cancel für laufende Stream-Befehle senden.
  5. Heartbeatping/pong senden, um langlebige Sessions am Leben zu halten.

Das Fehlen auch nur einer dieser Fähigkeiten verursacht typischerweise unzuverlässige Automatisierung bei Wiederverbindungen oder langlaufenden Streams.

HTTP-Bootstrap

Controller-Identität und Token-Status über HTTP herstellen, bevor WebSocket-Verkehr beginnt.

ZweckEndpunkt
Controller-Client registrierenPOST /api/controller/register
Anmeldeinformationen gegen Token-Paar eintauschenPOST /api/controller/token
Verbundene Nodes ermittelnGET /api/nodes/connected
Token-Paar aktualisierenPOST /api/auth/refresh

Client registrieren

POST /api/controller/register
Content-Type: application/json

{"name": "my-controller", "description": "Automatisierungs-Worker"}
{
"clientId": "clt_abc123",
"clientSecret": "cs_xxx",
"createdAt": 1776162000000
}
Warnung

Bewahren Sie clientSecret sicher auf. Das Relay speichert nur einen gesalzenen Hash — Sie können das Secret nach der Registrierung nicht wiederherstellen.

Access-Token ausstellen

POST /api/controller/token
Content-Type: application/json

{"clientId": "clt_abc123", "clientSecret": "cs_xxx"}
{
"clientId": "clt_abc123",
"controllerId": "ctl_123",
"accessToken": "<jwt>",
"refreshToken": "<refresh>"
}

Verbundene Nodes ermitteln

GET /api/nodes/connected
Authorization: Bearer <accessToken>
{
"nodes": [{"nodeId": "node_local_1"}]
}

Token aktualisieren

POST /api/auth/refresh
Content-Type: application/json

{"refreshToken": "<refresh>"}
{
"accessToken": "<new-jwt>",
"refreshToken": "<new-refresh>"
}

WebSocket-Auth-Sequenz

Nach dem HTTP-Bootstrap muss der WebSocket-Handshake dieser strikten Reihenfolge folgen:

Hello-Frame:

{
"protocolVersion": "1.0",
"messageType": "hello",
"requestId": "req_hello_1",
"timestamp": "2026-04-14T13:10:00.000Z",
"senderRole": "controller",
"payload": {"role": "controller", "capabilities": ["commands", "logs"]}
}

Auth-Frame:

{
"protocolVersion": "1.0",
"messageType": "auth",
"requestId": "req_auth_1",
"timestamp": "2026-04-14T13:10:00.020Z",
"senderRole": "controller",
"payload": {"accessToken": "<jwt>"}
}

Ping-Frame (Heartbeat, ca. alle 30s senden):

{
"protocolVersion": "1.0",
"messageType": "ping",
"requestId": "req_ping_1",
"timestamp": "2026-04-14T13:10:08.000Z",
"senderRole": "controller",
"payload": {"ts": 1776162608000}
}

Nicht authentifizierte Clients können keine Befehls-, Sperr- oder Abonnement-Frames senden.

Befehls-Envelope

FeldErforderlichHinweise
targetNodeIdJaRelay-Routing-Schlüssel; niemals weglassen
actionJaBefehlsaktion (z. B. command.run)
payloadJaAktions-Payload
replayNonceJaReplay-Schutz; eindeutigen Wert pro Anfrage verwenden
tabSessionIdAbhängigErforderlich für tab-bezogene Aktionen
waitPolicyOptionalfail_fast oder wait_with_timeout
timeoutMsOptionalBefehls-Timeout in Millisekunden

Listener und Streaming

Streaming verwendet einen zweiphasigen Ablauf:

  1. Befehlsphasecommand.test senden; Ergebnis-Envelope mit stream.listeners erhalten.
  2. Listener-Phase — pro Manifest-Eintrag abonnieren; asynchrone listener_update-Ereignisse verarbeiten, korreliert nach subscribe-requestId.

Subscribe-Frame-Beispiel:

{
"protocolVersion": "1.0",
"messageType": "command",
"requestId": "req_subscribe_1",
"senderRole": "controller",
"payload": {
"targetNodeId": "node_local_1",
"action": "listener.subscribe",
"payload": {
"listener": "network.http_intercept",
"options": { "tabSessionId": "ts_abc", "site": "reddit.com", "mode": "network" }
}
}
}

Beendigung muss explizit sein:

  • listener.unsubscribe mit der ursprünglichen subscribe-requestId senden.
  • command_cancel senden, das auf die ursprüngliche Stream-Befehls-requestId abzielt, für laufende Stream-Befehle.
Tipp

Halten Sie den WebSocket-Heartbeat während der gesamten Stream-Session aktiv. Veraltete Controller werden als getrennt behandelt und vom Relay bereinigt.

ACL und Node-Auswahl

targetNodeId ist immer erforderlich. Node-Besitzer kontrollieren ACL-Grants pro Controller-Client. Fehlende Grants schlagen deterministisch mit acl_missing_node_grant fehl. Gewähren Sie Zugriff über den Relay-ACL-Endpunkt oder über otto client-CLI-Befehle.

Wiederholungsrichtlinien

FehlertypWiederholungsstrategie
invalid_access_tokenToken aktualisieren, dann einmal wiederholen
lock_conflict / lock_timeoutBegrenztes Backoff, dann wiederholen
ValidierungsfehlerNicht wiederholen; Anfrage korrigieren
acl_missing_node_grantNicht wiederholen; Grant vom Node-Besitzer anfordern

Verwenden Sie Idempotenzschlüssel, wo anwendbar, damit sichere Wiederholungen zwischengespeicherte Endergebnisse zurückgeben, anstatt Seiteneffekte zu duplizieren.

Nächste Schritte