feat: add boards and tasks management endpoints
This commit is contained in:
0
backend/app/integrations/__init__.py
Normal file
0
backend/app/integrations/__init__.py
Normal file
@@ -1,282 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.db.session import engine
|
||||
from app.integrations.openclaw import OpenClawClient
|
||||
from app.models.org import Employee
|
||||
from app.models.projects import ProjectMember
|
||||
from app.models.work import Task, TaskComment
|
||||
|
||||
logger = logging.getLogger("app.notify")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NotifyContext:
|
||||
"""Notification context.
|
||||
|
||||
IMPORTANT: this is passed into FastAPI BackgroundTasks.
|
||||
Do not store live SQLAlchemy/SQLModel objects here; only ids/primitive data.
|
||||
"""
|
||||
|
||||
event: str # task.created | task.updated | task.assigned | comment.created | status.changed
|
||||
actor_employee_id: int
|
||||
task_id: int
|
||||
comment_id: int | None = None
|
||||
changed_fields: dict | None = None
|
||||
|
||||
|
||||
def _employees_with_session_keys(session: Session, employee_ids: Iterable[int]) -> list[Employee]:
|
||||
ids = sorted({i for i in employee_ids if i is not None})
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
emps = session.exec(select(Employee).where(Employee.id.in_(ids))).all()
|
||||
out: list[Employee] = []
|
||||
for e in emps:
|
||||
if not getattr(e, "notify_enabled", True):
|
||||
continue
|
||||
if getattr(e, "openclaw_session_key", None):
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
|
||||
def _project_pm_employee_ids(session: Session, project_id: int) -> set[int]:
|
||||
pms = session.exec(select(ProjectMember).where(ProjectMember.project_id == project_id)).all()
|
||||
pm_ids: set[int] = set()
|
||||
for m in pms:
|
||||
role = (m.role or "").lower()
|
||||
if role in {"pm", "product", "product_manager", "manager"}:
|
||||
pm_ids.add(m.employee_id)
|
||||
return pm_ids
|
||||
|
||||
|
||||
def resolve_recipients(
|
||||
session: Session, ctx: NotifyContext, task: Task, comment: TaskComment | None
|
||||
) -> set[int]:
|
||||
recipients: set[int] = set()
|
||||
|
||||
if ctx.event == "task.created":
|
||||
if task.assignee_employee_id:
|
||||
recipients.add(task.assignee_employee_id)
|
||||
recipients |= _project_pm_employee_ids(session, task.project_id)
|
||||
|
||||
elif ctx.event == "task.assigned":
|
||||
if task.assignee_employee_id:
|
||||
recipients.add(task.assignee_employee_id)
|
||||
recipients |= _project_pm_employee_ids(session, task.project_id)
|
||||
|
||||
elif ctx.event == "comment.created":
|
||||
if task.assignee_employee_id:
|
||||
recipients.add(task.assignee_employee_id)
|
||||
if task.reviewer_employee_id:
|
||||
recipients.add(task.reviewer_employee_id)
|
||||
recipients |= _project_pm_employee_ids(session, task.project_id)
|
||||
if comment and comment.author_employee_id:
|
||||
recipients.discard(comment.author_employee_id)
|
||||
|
||||
elif ctx.event == "status.changed":
|
||||
new_status = (getattr(task, "status", None) or "").lower()
|
||||
if new_status in {"review", "ready_for_review"} and task.reviewer_employee_id:
|
||||
recipients.add(task.reviewer_employee_id)
|
||||
recipients |= _project_pm_employee_ids(session, task.project_id)
|
||||
|
||||
elif ctx.event == "task.updated":
|
||||
recipients |= _project_pm_employee_ids(session, task.project_id)
|
||||
|
||||
recipients.discard(ctx.actor_employee_id)
|
||||
return recipients
|
||||
|
||||
|
||||
def ensure_employee_provisioned(session: Session, employee_id: int) -> None:
|
||||
"""Best-effort provisioning of a reviewer/manager so notifications can be delivered."""
|
||||
|
||||
emp = session.get(Employee, employee_id)
|
||||
if emp is None:
|
||||
return
|
||||
if not getattr(emp, "notify_enabled", True):
|
||||
return
|
||||
if getattr(emp, "openclaw_session_key", None):
|
||||
return
|
||||
|
||||
client = OpenClawClient.from_env()
|
||||
if client is None:
|
||||
logger.warning(
|
||||
"ensure_employee_provisioned: missing OpenClaw env", extra={"employee_id": employee_id}
|
||||
)
|
||||
return
|
||||
|
||||
prompt = (
|
||||
f"You are {emp.name} (employee_id={emp.id}).\n"
|
||||
"You are a reviewer/manager in Mission Control.\n\n"
|
||||
"Your job is to REVIEW work within the bounds of what the task requester asked for, using only the task + comments + current system state.\n"
|
||||
"Do NOT wait for the requester to provide more info by default.\n\n"
|
||||
"When a task is in review you must:\n"
|
||||
"1) Read the task title/description and all comments\n"
|
||||
"2) Verify the requested changes were actually made (check via Mission Control API if needed)\n"
|
||||
"3) Decide: approve or request changes\n"
|
||||
"4) Leave an audit comment explaining your decision (required)\n\n"
|
||||
"If something is ambiguous or missing, request changes with a clear checklist. Only ask the human if it's truly impossible to decide.\n"
|
||||
)
|
||||
|
||||
try:
|
||||
res = client.tools_invoke(
|
||||
"sessions_spawn",
|
||||
{"task": prompt, "label": f"employee:{emp.id}:{emp.name}"},
|
||||
timeout_s=20.0,
|
||||
)
|
||||
details = (res.get("result") or {}).get("details") or {}
|
||||
sk = details.get("childSessionKey") or details.get("sessionKey")
|
||||
if sk:
|
||||
emp.openclaw_session_key = sk
|
||||
session.add(emp)
|
||||
session.commit()
|
||||
logger.info(
|
||||
"ensure_employee_provisioned: provisioned",
|
||||
extra={"employee_id": emp.id, "session_key": sk},
|
||||
)
|
||||
except Exception:
|
||||
session.rollback()
|
||||
logger.exception("ensure_employee_provisioned: failed", extra={"employee_id": employee_id})
|
||||
|
||||
|
||||
def build_message(
|
||||
*,
|
||||
ctx: NotifyContext,
|
||||
task: Task,
|
||||
comment: TaskComment | None,
|
||||
recipient: Employee,
|
||||
base_url: str,
|
||||
) -> str:
|
||||
base = f"Task #{task.id}: {task.title}" if task.id is not None else f"Task: {task.title}"
|
||||
|
||||
if ctx.event in {"task.created", "task.assigned"} and recipient.employee_type == "agent":
|
||||
desc = (task.description or "").strip()
|
||||
if len(desc) > 500:
|
||||
desc = desc[:497] + "..."
|
||||
desc_block = f"\n\nDescription:\n{desc}" if desc else ""
|
||||
|
||||
return (
|
||||
f"{base}\n\n"
|
||||
f"Set BASE={base_url}\n\n"
|
||||
"You are the assignee. Start NOW (use the exec tool to run these curl commands):\n"
|
||||
f"1) curl -sS -X PATCH $BASE/tasks/{task.id} -H 'X-Actor-Employee-Id: {recipient.id}' -H 'Content-Type: application/json' -d '{{\"status\":\"in_progress\"}}'\n"
|
||||
f"2) curl -sS -X POST $BASE/task-comments -H 'X-Actor-Employee-Id: {recipient.id}' -H 'Content-Type: application/json' -d '{{\"task_id\":{task.id},\"body\":\"Plan: ... Next: ...\"}}'\n"
|
||||
"3) Do the work\n"
|
||||
"4) Post progress updates via POST $BASE/task-comments (same headers)\n"
|
||||
f"5) When complete: set status=review (assignee cannot set done) and wait for manager approval\n"
|
||||
f"{desc_block}"
|
||||
)
|
||||
|
||||
if ctx.event == "comment.created":
|
||||
snippet = ""
|
||||
if comment and comment.body:
|
||||
snippet = comment.body.strip().replace("\n", " ")
|
||||
if len(snippet) > 180:
|
||||
snippet = snippet[:177] + "..."
|
||||
snippet = f"\nComment: {snippet}"
|
||||
return f"New comment on {base}.{snippet}\nPlease review and respond in Mission Control."
|
||||
|
||||
if ctx.event == "status.changed":
|
||||
new_status = (getattr(task, "status", None) or "").lower()
|
||||
if new_status in {"review", "ready_for_review"}:
|
||||
return (
|
||||
f"Review requested for {base}.\n"
|
||||
"As the reviewer/manager, you must:\n"
|
||||
"1) Read the task + latest assignee comments\n"
|
||||
"2) Decide: approve or request changes\n"
|
||||
"3) Leave an audit comment explaining your decision (required)\n"
|
||||
f"4) Submit decision via POST /tasks/{task.id}/review (decision=approve|changes)\n"
|
||||
"Approve → task becomes done. Changes → task returns to in_progress and assignee is notified."
|
||||
)
|
||||
return (
|
||||
f"Status changed on {base} → {task.status}.\n"
|
||||
"Please review and respond in Mission Control."
|
||||
)
|
||||
|
||||
if ctx.event == "task.created":
|
||||
return f"New task created: {base}.\nPlease review and respond in Mission Control."
|
||||
|
||||
if ctx.event == "task.assigned":
|
||||
return f"Assigned: {base}.\nPlease review and respond in Mission Control."
|
||||
|
||||
return f"Update on {base}.\nPlease review and respond in Mission Control."
|
||||
|
||||
|
||||
def notify_openclaw(ctx: NotifyContext) -> None:
|
||||
"""Send OpenClaw notifications.
|
||||
|
||||
Runs in BackgroundTasks; opens its own DB session for safety.
|
||||
"""
|
||||
|
||||
client = OpenClawClient.from_env()
|
||||
logger.info(
|
||||
"notify_openclaw: start",
|
||||
extra={"event": ctx.event, "task_id": ctx.task_id, "actor": ctx.actor_employee_id},
|
||||
)
|
||||
if client is None:
|
||||
logger.warning("notify_openclaw: skipped (missing OpenClaw env)")
|
||||
return
|
||||
|
||||
with Session(engine) as session:
|
||||
task = session.get(Task, ctx.task_id)
|
||||
if task is None:
|
||||
logger.warning("notify_openclaw: task not found", extra={"task_id": ctx.task_id})
|
||||
return
|
||||
|
||||
comment = session.get(TaskComment, ctx.comment_id) if ctx.comment_id else None
|
||||
|
||||
if ctx.event == "status.changed":
|
||||
new_status = (getattr(task, "status", None) or "").lower()
|
||||
if new_status in {"review", "ready_for_review"} and task.reviewer_employee_id:
|
||||
ensure_employee_provisioned(session, int(task.reviewer_employee_id))
|
||||
|
||||
recipient_ids = resolve_recipients(session, ctx, task, comment)
|
||||
logger.info(
|
||||
"notify_openclaw: recipients resolved", extra={"recipient_ids": sorted(recipient_ids)}
|
||||
)
|
||||
recipients = _employees_with_session_keys(session, recipient_ids)
|
||||
if not recipients:
|
||||
logger.info("notify_openclaw: no recipients with session keys")
|
||||
return
|
||||
|
||||
# base URL used in agent messages
|
||||
base_url = __import__(
|
||||
"app.core.urls", fromlist=["public_api_base_url"]
|
||||
).public_api_base_url()
|
||||
|
||||
for e in recipients:
|
||||
sk = getattr(e, "openclaw_session_key", None)
|
||||
if not sk:
|
||||
continue
|
||||
|
||||
message = build_message(
|
||||
ctx=ctx,
|
||||
task=task,
|
||||
comment=comment,
|
||||
recipient=e,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
try:
|
||||
client.tools_invoke(
|
||||
"sessions_send",
|
||||
{"sessionKey": sk, "message": message},
|
||||
timeout_s=30.0,
|
||||
)
|
||||
except Exception:
|
||||
# keep the log, but avoid giant stack spam unless debugging
|
||||
logger.warning(
|
||||
"notify_openclaw: sessions_send failed",
|
||||
extra={
|
||||
"event": ctx.event,
|
||||
"task_id": ctx.task_id,
|
||||
"to_employee_id": getattr(e, "id", None),
|
||||
"session_key": sk,
|
||||
},
|
||||
)
|
||||
continue
|
||||
@@ -1,88 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from requests.exceptions import ReadTimeout, RequestException
|
||||
|
||||
logger = logging.getLogger("app.openclaw")
|
||||
|
||||
|
||||
class OpenClawClient:
|
||||
def __init__(self, base_url: str, token: str):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.token = token
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "OpenClawClient | None":
|
||||
# Ensure .env is loaded into os.environ (pydantic Settings reads env_file but
|
||||
# does not automatically populate os.environ).
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
url = os.environ.get("OPENCLAW_GATEWAY_URL")
|
||||
token = os.environ.get("OPENCLAW_GATEWAY_TOKEN")
|
||||
if not url or not token:
|
||||
return None
|
||||
return cls(url, token)
|
||||
|
||||
def tools_invoke(
|
||||
self,
|
||||
tool: str,
|
||||
args: dict[str, Any],
|
||||
*,
|
||||
session_key: str | None = None,
|
||||
timeout_s: float = 10.0,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"tool": tool, "args": args}
|
||||
logger.info(
|
||||
"openclaw.tools_invoke",
|
||||
extra={"tool": tool, "has_session_key": bool(session_key), "timeout_s": timeout_s},
|
||||
)
|
||||
if session_key is not None:
|
||||
payload["sessionKey"] = session_key
|
||||
|
||||
last_err: Exception | None = None
|
||||
# Retry a few times; the gateway can be busy and respond slowly.
|
||||
for attempt in range(4):
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{self.base_url}/tools/invoke",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=payload,
|
||||
# connect timeout, read timeout
|
||||
timeout=(2.0, timeout_s),
|
||||
)
|
||||
r.raise_for_status()
|
||||
logger.info(
|
||||
"openclaw.tools_invoke: ok",
|
||||
extra={"tool": tool, "status": r.status_code, "attempt": attempt + 1},
|
||||
)
|
||||
return r.json()
|
||||
except ReadTimeout as e:
|
||||
last_err = e
|
||||
logger.warning(
|
||||
"openclaw.tools_invoke: timeout",
|
||||
extra={"tool": tool, "attempt": attempt + 1, "timeout_s": timeout_s},
|
||||
)
|
||||
time.sleep(0.5 * (2**attempt))
|
||||
except RequestException as e:
|
||||
last_err = e
|
||||
logger.warning(
|
||||
"openclaw.tools_invoke: request error",
|
||||
extra={"tool": tool, "attempt": attempt + 1, "error": str(e)},
|
||||
)
|
||||
time.sleep(0.5 * (2**attempt))
|
||||
|
||||
assert last_err is not None
|
||||
raise last_err
|
||||
@@ -1,129 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from app.integrations.openclaw import OpenClawClient
|
||||
|
||||
|
||||
def _slug(s: str) -> str:
|
||||
s = (s or "").strip().lower()
|
||||
s = re.sub(r"[^a-z0-9]+", "-", s)
|
||||
s = re.sub(r"-+", "-", s).strip("-")
|
||||
return s or "agent"
|
||||
|
||||
|
||||
def desired_agent_id(*, employee_id: int, name: str) -> str:
|
||||
return f"employee-{employee_id}-{_slug(name)}"
|
||||
|
||||
|
||||
def ensure_full_agent_profile(
|
||||
*,
|
||||
client: OpenClawClient,
|
||||
employee_id: int,
|
||||
employee_name: str,
|
||||
) -> dict[str, str]:
|
||||
"""Ensure an OpenClaw agent profile exists for this employee.
|
||||
|
||||
Returns {"agent_id": ..., "workspace": ...}.
|
||||
|
||||
Implementation strategy:
|
||||
- Create per-agent workspace + agent dir on the gateway host.
|
||||
- Add/ensure entry in openclaw.json agents.list.
|
||||
|
||||
NOTE: This uses OpenClaw gateway tools via /tools/invoke (gateway + exec).
|
||||
"""
|
||||
|
||||
agent_id = desired_agent_id(employee_id=employee_id, name=employee_name)
|
||||
|
||||
workspace = f"/home/asaharan/.openclaw/workspaces/{agent_id}"
|
||||
agent_dir = f"/home/asaharan/.openclaw/agents/{agent_id}/agent"
|
||||
|
||||
# 1) Create dirs
|
||||
client.tools_invoke(
|
||||
"exec",
|
||||
{
|
||||
"command": f"mkdir -p {workspace} {agent_dir}",
|
||||
},
|
||||
timeout_s=20.0,
|
||||
)
|
||||
|
||||
# 2) Write minimal identity files in the per-agent workspace
|
||||
identity_md = (
|
||||
"# IDENTITY.md\n\n"
|
||||
"- **Name:** " + employee_name + "\n"
|
||||
"- **Creature:** AI agent employee (Mission Control)\n"
|
||||
"- **Vibe:** Direct, action-oriented, leaves audit trails\n"
|
||||
)
|
||||
user_md = (
|
||||
"# USER.md\n\n"
|
||||
"You work for Abhimanyu.\n"
|
||||
"You must execute Mission Control tasks via the API and keep state synced.\n"
|
||||
)
|
||||
|
||||
# Use cat heredocs to avoid dependency on extra tooling.
|
||||
client.tools_invoke(
|
||||
"exec",
|
||||
{
|
||||
"command": "bash -lc "
|
||||
+ json.dumps(
|
||||
"""
|
||||
cat > {ws}/IDENTITY.md <<'EOF'
|
||||
{identity}
|
||||
EOF
|
||||
cat > {ws}/USER.md <<'EOF'
|
||||
{user}
|
||||
EOF
|
||||
""".format(ws=workspace, identity=identity_md, user=user_md)
|
||||
),
|
||||
},
|
||||
timeout_s=20.0,
|
||||
)
|
||||
|
||||
# 3) Update openclaw.json agents.list (idempotent)
|
||||
cfg_resp = client.tools_invoke("gateway", {"action": "config.get"}, timeout_s=20.0)
|
||||
raw = (
|
||||
(((cfg_resp or {}).get("result") or {}).get("content") or [{}])[0].get("text")
|
||||
if isinstance((((cfg_resp or {}).get("result") or {}).get("content") or [{}]), list)
|
||||
else None
|
||||
)
|
||||
|
||||
if not raw:
|
||||
# fallback: tool may return {ok:true,result:{raw:...}}
|
||||
raw = ((cfg_resp.get("result") or {}).get("raw")) if isinstance(cfg_resp, dict) else None
|
||||
|
||||
if not raw:
|
||||
raise RuntimeError("Unable to read gateway config via tools")
|
||||
|
||||
cfg = json.loads(raw)
|
||||
|
||||
agents = cfg.get("agents") or {}
|
||||
agents_list = agents.get("list") or []
|
||||
if not isinstance(agents_list, list):
|
||||
agents_list = []
|
||||
|
||||
exists = any(isinstance(a, dict) and a.get("id") == agent_id for a in agents_list)
|
||||
if not exists:
|
||||
agents_list.append(
|
||||
{
|
||||
"id": agent_id,
|
||||
"name": employee_name,
|
||||
"workspace": workspace,
|
||||
"agentDir": agent_dir,
|
||||
"identity": {"name": employee_name, "emoji": "🜁"},
|
||||
}
|
||||
)
|
||||
agents["list"] = agents_list
|
||||
cfg["agents"] = agents
|
||||
|
||||
client.tools_invoke(
|
||||
"gateway",
|
||||
{"action": "config.apply", "raw": json.dumps(cfg)},
|
||||
timeout_s=30.0,
|
||||
)
|
||||
# give the gateway a moment to reload the agent registry
|
||||
time.sleep(2.5)
|
||||
|
||||
return {"agent_id": agent_id, "workspace": workspace}
|
||||
126
backend/app/integrations/openclaw_gateway.py
Normal file
126
backend/app/integrations/openclaw_gateway.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode, urlparse, urlunparse
|
||||
from uuid import uuid4
|
||||
|
||||
import websockets
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class OpenClawGatewayError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenClawResponse:
|
||||
payload: Any
|
||||
|
||||
|
||||
def _build_gateway_url() -> str:
|
||||
base_url = settings.openclaw_gateway_url or "ws://127.0.0.1:18789"
|
||||
token = settings.openclaw_gateway_token
|
||||
if not token:
|
||||
return base_url
|
||||
parsed = urlparse(base_url)
|
||||
query = urlencode({"token": token})
|
||||
return urlunparse(parsed._replace(query=query))
|
||||
|
||||
|
||||
async def _await_response(ws: websockets.WebSocketClientProtocol, request_id: str) -> Any:
|
||||
while True:
|
||||
raw = await ws.recv()
|
||||
data = json.loads(raw)
|
||||
|
||||
if data.get("type") == "res" and data.get("id") == request_id:
|
||||
if data.get("ok") is False:
|
||||
error = data.get("error", {}).get("message", "Gateway error")
|
||||
raise OpenClawGatewayError(error)
|
||||
return data.get("payload")
|
||||
|
||||
if data.get("id") == request_id:
|
||||
if data.get("error"):
|
||||
raise OpenClawGatewayError(data["error"].get("message", "Gateway error"))
|
||||
return data.get("result")
|
||||
|
||||
|
||||
async def _send_request(
|
||||
ws: websockets.WebSocketClientProtocol, method: str, params: dict[str, Any] | None
|
||||
) -> Any:
|
||||
request_id = str(uuid4())
|
||||
message = {"type": "req", "id": request_id, "method": method, "params": params or {}}
|
||||
await ws.send(json.dumps(message))
|
||||
return await _await_response(ws, request_id)
|
||||
|
||||
|
||||
async def _handle_challenge(
|
||||
ws: websockets.WebSocketClientProtocol, first_message: str | None
|
||||
) -> None:
|
||||
if not first_message:
|
||||
return
|
||||
data = json.loads(first_message)
|
||||
if data.get("type") != "event" or data.get("event") != "connect.challenge":
|
||||
return
|
||||
|
||||
connect_id = str(uuid4())
|
||||
response = {
|
||||
"type": "req",
|
||||
"id": connect_id,
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 3,
|
||||
"client": {
|
||||
"id": "gateway-client",
|
||||
"version": "1.0.0",
|
||||
"platform": "web",
|
||||
"mode": "ui",
|
||||
},
|
||||
"auth": {"token": settings.openclaw_gateway_token},
|
||||
},
|
||||
}
|
||||
await ws.send(json.dumps(response))
|
||||
await _await_response(ws, connect_id)
|
||||
|
||||
|
||||
async def openclaw_call(method: str, params: dict[str, Any] | None = None) -> Any:
|
||||
gateway_url = _build_gateway_url()
|
||||
try:
|
||||
async with websockets.connect(gateway_url, ping_interval=None) as ws:
|
||||
first_message = None
|
||||
try:
|
||||
first_message = await asyncio.wait_for(ws.recv(), timeout=2)
|
||||
except asyncio.TimeoutError:
|
||||
first_message = None
|
||||
await _handle_challenge(ws, first_message)
|
||||
return await _send_request(ws, method, params)
|
||||
except OpenClawGatewayError:
|
||||
raise
|
||||
except Exception as exc: # pragma: no cover - network errors
|
||||
raise OpenClawGatewayError(str(exc)) from exc
|
||||
|
||||
|
||||
async def send_message(
|
||||
message: str,
|
||||
*,
|
||||
session_key: str,
|
||||
deliver: bool = False,
|
||||
) -> Any:
|
||||
params: dict[str, Any] = {
|
||||
"sessionKey": session_key,
|
||||
"message": message,
|
||||
"deliver": deliver,
|
||||
"idempotencyKey": str(uuid4()),
|
||||
}
|
||||
return await openclaw_call("chat.send", params)
|
||||
|
||||
|
||||
async def get_chat_history(session_key: str, limit: int | None = None) -> Any:
|
||||
params: dict[str, Any] = {"sessionKey": session_key}
|
||||
if limit is not None:
|
||||
params["limit"] = limit
|
||||
return await openclaw_call("chat.history", params)
|
||||
Reference in New Issue
Block a user