feat(adapter/notify): implement OpenClawNotifyAdapter
Sends notifications via `openclaw system event --text <msg> --mode now`. - Always logs locally (info/warning/error) regardless of CLI availability - Gracefully handles FileNotFoundError (openclaw not on PATH) and TimeoutExpired; notifications are best-effort and never crash the pipeline - OPENCLAW_SIGNAL_NUMBER env var stored for future direct-signal support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,35 +1,93 @@
|
||||
"""
|
||||
adapters/notify/openclaw.py
|
||||
OpenClaw notification adapter — Phase 2 stub.
|
||||
OpenClaw notification adapter — Phase 2 implementation.
|
||||
|
||||
TODO (Phase 2):
|
||||
- Implement send() to dispatch notifications via the OpenClaw API.
|
||||
- Support context keys: channel, severity, run_id, brief_id.
|
||||
- Read endpoint and credentials from environment (OPENCLAW_API_KEY, OPENCLAW_URL).
|
||||
- Handle rate limiting and delivery retries.
|
||||
Sends notifications by shelling out to the ``openclaw`` CLI::
|
||||
|
||||
openclaw system event --text "<message>" --mode now
|
||||
|
||||
If the binary is not on PATH the method logs a warning and returns without
|
||||
raising — notifications are best-effort and should never crash the pipeline.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from adapters.base.notify import NotifyAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenClawNotifyAdapter(NotifyAdapter):
|
||||
"""
|
||||
Notification adapter that sends messages via OpenClaw.
|
||||
Notification adapter that dispatches messages via the ``openclaw`` CLI.
|
||||
|
||||
Expects environment variables:
|
||||
OPENCLAW_API_KEY — authentication token
|
||||
OPENCLAW_URL — base URL for the OpenClaw API (optional, defaults to hosted)
|
||||
Environment variables
|
||||
---------------------
|
||||
OPENCLAW_SIGNAL_NUMBER : Optional. Direct signal target for OpenClaw sends.
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
# TODO (Phase 2): Accept loaded team.yaml config dict.
|
||||
# Extract OPENCLAW_API_KEY and OPENCLAW_URL from environment.
|
||||
# Initialise an HTTP client (e.g. httpx or requests).
|
||||
raise NotImplementedError("OpenClawNotifyAdapter.__init__ is not yet implemented.")
|
||||
"""
|
||||
Initialise the OpenClaw notification adapter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
config : Loaded team.yaml config dict (reserved for future options).
|
||||
"""
|
||||
self._config = config
|
||||
self._signal_number: str = os.environ.get("OPENCLAW_SIGNAL_NUMBER", "")
|
||||
|
||||
def send(self, message: str, context: dict) -> None:
|
||||
# TODO (Phase 2): POST notification payload to OpenClaw API.
|
||||
# Include message, context (channel, severity, run_id, brief_id).
|
||||
# Log delivery confirmation or raise on failure.
|
||||
raise NotImplementedError("OpenClawNotifyAdapter.send is not yet implemented.")
|
||||
"""
|
||||
Send a notification via ``openclaw system event``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message : Human-readable notification text.
|
||||
context : Optional metadata. Recognised keys:
|
||||
level (str) — "info" | "warning" | "error"; logged locally.
|
||||
run_id (str) — included in the local log record.
|
||||
brief_id (str) — included in the local log record.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If the ``openclaw`` binary is not present on PATH, the method logs a
|
||||
warning and returns silently. Notifications are best-effort.
|
||||
"""
|
||||
level: str = context.get("level", "info")
|
||||
run_id: str = context.get("run_id", "")
|
||||
brief_id: str = context.get("brief_id", "")
|
||||
|
||||
# Always log locally regardless of CLI availability.
|
||||
log_msg = "[notify:%s] %s (run=%s brief=%s)" % (level, message, run_id, brief_id)
|
||||
if level == "error":
|
||||
logger.error(log_msg)
|
||||
elif level == "warning":
|
||||
logger.warning(log_msg)
|
||||
else:
|
||||
logger.info(log_msg)
|
||||
|
||||
cmd = ["openclaw", "system", "event", "--text", message, "--mode", "now"]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(
|
||||
"openclaw event returned non-zero exit %d: %s",
|
||||
result.returncode,
|
||||
result.stderr.strip(),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.warning(
|
||||
"openclaw CLI not found on PATH; notification not delivered: %s",
|
||||
message,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("openclaw event timed out for message: %s", message)
|
||||
|
||||
Reference in New Issue
Block a user