fix: show websocket cron jobs in automations

This commit is contained in:
chengyongru 2026-06-11 23:53:45 +08:00
parent 3725b42e0e
commit 8dac6b2889
3 changed files with 38 additions and 24 deletions

View File

@ -4,29 +4,26 @@ from __future__ import annotations
from typing import Any, Protocol from typing import Any, Protocol
from nanobot.cron.automation import is_bound_agent_job
from nanobot.cron.types import CronJob from nanobot.cron.types import CronJob
class _CronServiceLike(Protocol): class _CronServiceLike(Protocol):
def list_bound_agent_jobs_for_session( def list_jobs(self, *, include_disabled: bool = False) -> list[CronJob]: ...
self,
session_key: str,
*,
include_disabled: bool = True,
) -> list[CronJob]: ...
def bound_session_automation_jobs( def session_automation_jobs(
cron_service: _CronServiceLike | None, cron_service: _CronServiceLike | None,
session_key: str, session_key: str,
) -> list[CronJob]: ) -> 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: if cron_service is None:
return [] return []
return cron_service.list_bound_agent_jobs_for_session( return [
session_key, job
include_disabled=True, for job in cron_service.list_jobs(include_disabled=True)
) if _matches_webui_session(job, session_key)
]
def session_automations_payload( def session_automations_payload(
@ -34,7 +31,22 @@ def session_automations_payload(
session_key: str, session_key: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return user-created automation jobs attached to a WebUI session.""" """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]]: def serialize_automation_jobs(jobs: list[CronJob]) -> list[dict[str, Any]]:

View File

@ -62,8 +62,8 @@ from nanobot.webui.http_utils import (
) )
from nanobot.webui.media_gateway import WebUIMediaGateway from nanobot.webui.media_gateway import WebUIMediaGateway
from nanobot.webui.session_automations import ( from nanobot.webui.session_automations import (
bound_session_automation_jobs,
serialize_automation_jobs, serialize_automation_jobs,
session_automation_jobs,
session_automations_payload, session_automations_payload,
) )
from nanobot.webui.session_list_index import list_webui_sessions from nanobot.webui.session_list_index import list_webui_sessions
@ -452,17 +452,17 @@ class GatewayHTTPHandler:
return _http_error(404, "session not found") return _http_error(404, "session not found")
query = _parse_query(request.path) query = _parse_query(request.path)
delete_automations = (_query_first(query, "delete_automations") or "").lower() delete_automations = (_query_first(query, "delete_automations") or "").lower()
bound_jobs = bound_session_automation_jobs(self.cron_service, decoded_key) automation_jobs = session_automation_jobs(self.cron_service, decoded_key)
if bound_jobs and delete_automations not in {"1", "true", "yes"}: if automation_jobs and delete_automations not in {"1", "true", "yes"}:
return _http_json_response( return _http_json_response(
{ {
"deleted": False, "deleted": False,
"blocked_by_automations": True, "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: if automation_jobs and self.cron_service is not None:
for job in bound_jobs: for job in automation_jobs:
self.cron_service.remove_job(job.id) self.cron_service.remove_job(job.id)
deleted = self.session_manager.delete_session(decoded_key) deleted = self.session_manager.delete_session(decoded_key)
delete_webui_thread(decoded_key) delete_webui_thread(decoded_key)

View File

@ -189,7 +189,7 @@ async def test_session_automations_route_filters_by_webui_session(
cron.add_job( cron.add_job(
name="Legacy same target", name="Legacy same target",
schedule=hourly, schedule=hourly,
message="Legacy job should not be treated as bound", message="Legacy job should still show in WebUI",
deliver=True, deliver=True,
channel="websocket", channel="websocket",
to="abc", to="abc",
@ -227,11 +227,15 @@ async def test_session_automations_route_filters_by_webui_session(
assert resp.status_code == 200 assert resp.status_code == 200
body = resp.json() 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] job = body["jobs"][0]
assert job["schedule"]["kind"] == "every" assert job["schedule"]["kind"] == "every"
assert job["schedule"]["every_ms"] == 3_600_000 assert job["schedule"]["every_ms"] == 3_600_000
assert job["payload"]["message"] == "Check the project status" assert job["payload"]["message"] == "Check the project status"
assert body["jobs"][1]["payload"]["message"] == "Legacy job should still show in WebUI"
finally: finally:
await channel.stop() await channel.stop()
await server_task await server_task
@ -743,9 +747,7 @@ async def test_session_delete_can_cascade_bound_automations(
assert resp.json()["deleted"] is True assert resp.json()["deleted"] is True
assert not path.exists() assert not path.exists()
assert cron.list_bound_agent_jobs_for_session("websocket:doomed") == [] assert cron.list_bound_agent_jobs_for_session("websocket:doomed") == []
assert [job.name for job in cron.list_jobs(include_disabled=True)] == [ assert cron.list_jobs(include_disabled=True) == []
"Legacy same target"
]
finally: finally:
await channel.stop() await channel.stop()
await server_task await server_task