From 8dac6b288900ae8cb9f9a3ae358837f71b7ea915 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Thu, 11 Jun 2026 23:53:45 +0800 Subject: [PATCH] fix: show websocket cron jobs in automations --- nanobot/webui/session_automations.py | 38 +++++++++++++------- nanobot/webui/ws_http.py | 12 +++---- tests/channels/test_websocket_http_routes.py | 12 ++++--- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/nanobot/webui/session_automations.py b/nanobot/webui/session_automations.py index 8a57b9442..c60f05735 100644 --- a/nanobot/webui/session_automations.py +++ b/nanobot/webui/session_automations.py @@ -4,29 +4,26 @@ from __future__ import annotations from typing import Any, Protocol +from nanobot.cron.automation import is_bound_agent_job from nanobot.cron.types import CronJob class _CronServiceLike(Protocol): - def list_bound_agent_jobs_for_session( - self, - session_key: str, - *, - include_disabled: bool = True, - ) -> list[CronJob]: ... + def list_jobs(self, *, include_disabled: bool = False) -> list[CronJob]: ... -def bound_session_automation_jobs( +def session_automation_jobs( cron_service: _CronServiceLike | None, session_key: str, ) -> list[CronJob]: - """Return agent-turn automation jobs explicitly bound to *session_key*.""" + """Return user automations attached to the WebUI session.""" if cron_service is None: return [] - return cron_service.list_bound_agent_jobs_for_session( - session_key, - include_disabled=True, - ) + return [ + job + for job in cron_service.list_jobs(include_disabled=True) + if _matches_webui_session(job, session_key) + ] def session_automations_payload( @@ -34,7 +31,22 @@ def session_automations_payload( session_key: str, ) -> dict[str, Any]: """Return user-created automation jobs attached to a WebUI session.""" - return {"jobs": serialize_automation_jobs(bound_session_automation_jobs(cron_service, session_key))} + return { + "jobs": serialize_automation_jobs(session_automation_jobs(cron_service, session_key)) + } + + +def _matches_webui_session(job: CronJob, session_key: str) -> bool: + payload = job.payload + if payload.kind != "agent_turn": + return False + if is_bound_agent_job(job): + return payload.session_key == session_key + return bool( + payload.channel == "websocket" + and payload.to + and session_key == f"websocket:{payload.to}" + ) def serialize_automation_jobs(jobs: list[CronJob]) -> list[dict[str, Any]]: diff --git a/nanobot/webui/ws_http.py b/nanobot/webui/ws_http.py index e0e0d321f..70e19e01b 100644 --- a/nanobot/webui/ws_http.py +++ b/nanobot/webui/ws_http.py @@ -62,8 +62,8 @@ from nanobot.webui.http_utils import ( ) from nanobot.webui.media_gateway import WebUIMediaGateway from nanobot.webui.session_automations import ( - bound_session_automation_jobs, serialize_automation_jobs, + session_automation_jobs, session_automations_payload, ) from nanobot.webui.session_list_index import list_webui_sessions @@ -452,17 +452,17 @@ class GatewayHTTPHandler: return _http_error(404, "session not found") query = _parse_query(request.path) delete_automations = (_query_first(query, "delete_automations") or "").lower() - bound_jobs = bound_session_automation_jobs(self.cron_service, decoded_key) - if bound_jobs and delete_automations not in {"1", "true", "yes"}: + automation_jobs = session_automation_jobs(self.cron_service, decoded_key) + if automation_jobs and delete_automations not in {"1", "true", "yes"}: return _http_json_response( { "deleted": False, "blocked_by_automations": True, - "automations": serialize_automation_jobs(bound_jobs), + "automations": serialize_automation_jobs(automation_jobs), } ) - if bound_jobs and self.cron_service is not None: - for job in bound_jobs: + if automation_jobs and self.cron_service is not None: + for job in automation_jobs: self.cron_service.remove_job(job.id) deleted = self.session_manager.delete_session(decoded_key) delete_webui_thread(decoded_key) diff --git a/tests/channels/test_websocket_http_routes.py b/tests/channels/test_websocket_http_routes.py index bc11c2e15..c3614c7e2 100644 --- a/tests/channels/test_websocket_http_routes.py +++ b/tests/channels/test_websocket_http_routes.py @@ -189,7 +189,7 @@ async def test_session_automations_route_filters_by_webui_session( cron.add_job( name="Legacy same target", schedule=hourly, - message="Legacy job should not be treated as bound", + message="Legacy job should still show in WebUI", deliver=True, channel="websocket", to="abc", @@ -227,11 +227,15 @@ async def test_session_automations_route_filters_by_webui_session( assert resp.status_code == 200 body = resp.json() - assert [job["name"] for job in body["jobs"]] == ["Morning check"] + assert [job["name"] for job in body["jobs"]] == [ + "Morning check", + "Legacy same target", + ] job = body["jobs"][0] assert job["schedule"]["kind"] == "every" assert job["schedule"]["every_ms"] == 3_600_000 assert job["payload"]["message"] == "Check the project status" + assert body["jobs"][1]["payload"]["message"] == "Legacy job should still show in WebUI" finally: await channel.stop() await server_task @@ -743,9 +747,7 @@ async def test_session_delete_can_cascade_bound_automations( assert resp.json()["deleted"] is True assert not path.exists() assert cron.list_bound_agent_jobs_for_session("websocket:doomed") == [] - assert [job.name for job in cron.list_jobs(include_disabled=True)] == [ - "Legacy same target" - ] + assert cron.list_jobs(include_disabled=True) == [] finally: await channel.stop() await server_task