diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index c86fc160f..5b2c6ee9e 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -22,6 +22,12 @@ if TYPE_CHECKING: from astrbot.core.star.context import Context +class CronJobSchedulingError(Exception): + """Raised when a cron job fails to be scheduled.""" + + pass + + class CronJobManager: """Central scheduler for BasicCronJob and ActiveAgentCronJob.""" @@ -59,7 +65,10 @@ class CronJobManager: job.job_id, ) continue - self._schedule_job(job) + try: + self._schedule_job(job) + except CronJobSchedulingError: + continue # Error already logged in _schedule_job async def add_basic_job( self, @@ -181,8 +190,9 @@ class CronJobManager: job.job_id, next_run_time=self._get_next_run_time(job.job_id) ) ) - except Exception as e: - logger.error(f"Failed to schedule cron job {job.job_id}: {e!s}") + except (ValueError, TypeError) as e: + logger.exception("Failed to schedule cron job %s", job.job_id) + raise CronJobSchedulingError(str(e)) from e def _get_next_run_time(self, job_id: str): aps_job = self.scheduler.get_job(job_id) diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py index d0b563816..bbfa5729d 100644 --- a/astrbot/core/tools/cron_tools.py +++ b/astrbot/core/tools/cron_tools.py @@ -7,6 +7,7 @@ from pydantic.dataclasses import dataclass from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import FunctionTool, ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext +from astrbot.core.cron.manager import CronJobSchedulingError from astrbot.core.tools.registry import builtin_tool _CRON_TOOL_CONFIG = { @@ -112,14 +113,17 @@ class FutureTaskTool(FunctionTool[AstrAgentContext]): "origin": "tool", } - job = await cron_mgr.add_active_job( - name=name, - cron_expression=str(cron_expression) if cron_expression else None, - payload=payload, - description=note, - run_once=run_once, - run_at=run_at_dt, - ) + try: + job = await cron_mgr.add_active_job( + name=name, + cron_expression=str(cron_expression) if cron_expression else None, + payload=payload, + description=note, + run_once=run_once, + run_at=run_at_dt, + ) + except CronJobSchedulingError: + return "error: failed to schedule task due to invalid configuration." next_run = job.next_run_time or run_at_dt suffix = ( f"one-time at {next_run}" @@ -195,7 +199,10 @@ class FutureTaskTool(FunctionTool[AstrAgentContext]): updates["cron_expression"] = cron_expression updates["payload"] = payload - job = await cron_mgr.update_job(str(job_id), **updates) + try: + job = await cron_mgr.update_job(str(job_id), **updates) + except CronJobSchedulingError: + return "error: failed to update task due to invalid configuration." if not job: return f"error: cron job {job_id} not found." return f"Updated future task {job.job_id} ({job.name})." diff --git a/tests/unit/test_cron_manager.py b/tests/unit/test_cron_manager.py index b111384ac..9dd3fc34d 100644 --- a/tests/unit/test_cron_manager.py +++ b/tests/unit/test_cron_manager.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from astrbot.core.cron.manager import CronJobManager +from astrbot.core.cron.manager import CronJobManager, CronJobSchedulingError from astrbot.core.db.po import CronJob @@ -190,24 +190,25 @@ class TestAddActiveJob: @pytest.mark.asyncio async def test_add_active_job_run_once(self, cron_manager, mock_db, sample_cron_job): - """Test adding a run-once active job.""" + """Test adding a run-once active job with an invalid returned job.""" sample_cron_job.job_type = "active_agent" sample_cron_job.run_once = True mock_db.create_cron_job.return_value = sample_cron_job run_at = datetime.now(timezone.utc) + timedelta(days=30) - result = await cron_manager.add_active_job( - name="Test Run Once Job", - cron_expression=None, - payload={"session": "test:group:123"}, - run_once=True, - run_at=run_at, - ) + with pytest.raises(CronJobSchedulingError, match="Invalid isoformat string"): + await cron_manager.add_active_job( + name="Test Run Once Job", + cron_expression=None, + payload={"session": "test:group:123"}, + run_once=True, + run_at=run_at, + ) - assert result == sample_cron_job call_kwargs = mock_db.create_cron_job.call_args.kwargs assert call_kwargs["run_once"] is True + assert call_kwargs["payload"]["run_at"] == run_at.isoformat() class TestUpdateJob: