From 1ab23768f7a6801e14cb6416aebde1a65a851341 Mon Sep 17 00:00:00 2001 From: Xinbo Chen <91771595+fxquarter@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:06:46 +0800 Subject: [PATCH] fix cron active agent fallback delivery --- astrbot/core/cron/manager.py | 76 +++++++++++++++++++++++++++++++ tests/unit/test_cron_manager.py | 81 ++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index aa11bb601f..ac1f4faaee 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -14,6 +14,7 @@ from astrbot.core.cron.events import CronMessageEvent from astrbot.core.db import BaseDatabase from astrbot.core.db.po import CronJob +from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.message_session import MessageSession from astrbot.core.provider.entites import ProviderRequest from astrbot.core.utils.history_saver import persist_agent_history @@ -386,9 +387,84 @@ async def _woke_main_agent( req=req, summary_note=summary_note, ) + await self._send_active_agent_fallback_if_needed( + session=session, + req=req, + llm_resp=llm_resp, + cron_meta=cron_meta, + ) if not llm_resp: logger.warning("Cron job agent got no response") return + async def _send_active_agent_fallback_if_needed( + self, + *, + session: MessageSession, + req: ProviderRequest, + llm_resp, + cron_meta: dict, + ) -> bool: + if self._agent_sent_message_to_user(req): + logger.info( + "cron active agent fallback skipped agent_sent=True session=%s job_id=%s", + session, + cron_meta.get("id"), + ) + return False + + text = str(getattr(llm_resp, "completion_text", "") or "").strip() + if not llm_resp or getattr(llm_resp, "role", "") != "assistant" or not text: + logger.warning( + "cron active agent fallback skipped no assistant text session=%s job_id=%s", + session, + cron_meta.get("id"), + ) + return False + + logger.info( + "cron active agent fallback send start session=%s job_id=%s", + session, + cron_meta.get("id"), + ) + try: + ok = await self.ctx.send_message(session, MessageChain().message(text)) + logger.info( + "cron active agent fallback send done ok=%s session=%s job_id=%s", + ok, + session, + cron_meta.get("id"), + ) + return bool(ok) + except Exception as e: # noqa: BLE001 + logger.warning( + "cron active agent fallback send exception session=%s job_id=%s err=%r", + session, + cron_meta.get("id"), + e, + exc_info=True, + ) + raise + + @staticmethod + def _agent_sent_message_to_user(req: ProviderRequest) -> bool: + results = getattr(req, "tool_calls_result", None) + if not results: + return False + if not isinstance(results, list): + results = [results] + + for result in results: + call_results = getattr(result, "tool_calls_result", None) or [] + for call_result in call_results: + content = getattr(call_result, "content", "") + if isinstance(content, list): + content = " ".join( + str(getattr(part, "text", part)) for part in content + ) + if "Message sent to session" in str(content): + return True + return False + __all__ = ["CronJobManager"] diff --git a/tests/unit/test_cron_manager.py b/tests/unit/test_cron_manager.py index 9dd3fc34dc..5ed8a7e3f0 100644 --- a/tests/unit/test_cron_manager.py +++ b/tests/unit/test_cron_manager.py @@ -1,12 +1,16 @@ """Tests for CronJobManager.""" from datetime import datetime, timedelta, timezone +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest from astrbot.core.cron.manager import CronJobManager, CronJobSchedulingError from astrbot.core.db.po import CronJob +from astrbot.core.platform.message_session import MessageSession +from astrbot.core.platform.message_type import MessageType +from astrbot.core.provider.entites import ProviderRequest @pytest.fixture @@ -215,7 +219,7 @@ class TestUpdateJob: """Tests for update_job method.""" @pytest.mark.asyncio - async def test_update_job(self, cron_manager, mock_db, sample_cron_job): + async def test_update_job(self, cron_manager, mock_db): """Test updating a cron job.""" updated_job = CronJob( job_id="test-job-id", @@ -482,6 +486,81 @@ async def test_run_basic_job_no_handler(self, cron_manager, sample_cron_job): await cron_manager._run_basic_job(sample_cron_job) +class TestActiveAgentFallback: + """Tests for active-agent cron fallback delivery.""" + + @pytest.mark.asyncio + async def test_fallback_sends_final_text_when_agent_did_not_send_message( + self, cron_manager, mock_context + ): + cron_manager.ctx = mock_context + mock_context.send_message = AsyncMock(return_value=True) + session = MessageSession( + "test-platform", + MessageType.FRIEND_MESSAGE, + "session-1", + ) + req = ProviderRequest() + llm_resp = SimpleNamespace( + role="assistant", + completion_text="定时任务完成。", + ) + + sent = await cron_manager._send_active_agent_fallback_if_needed( + session=session, + req=req, + llm_resp=llm_resp, + cron_meta={"id": "job-1", "name": "job"}, + ) + + assert sent is True + mock_context.send_message.assert_awaited_once() + assert mock_context.send_message.await_args.args[0] == session + assert ( + mock_context.send_message.await_args.args[1].chain[0].text + == "定时任务完成。" + ) + + @pytest.mark.asyncio + async def test_fallback_skips_when_agent_already_sent_message( + self, cron_manager, mock_context + ): + cron_manager.ctx = mock_context + mock_context.send_message = AsyncMock(return_value=True) + session = MessageSession( + "test-platform", + MessageType.FRIEND_MESSAGE, + "session-1", + ) + req = ProviderRequest() + req.tool_calls_result = [ + SimpleNamespace( + tool_calls_result=[ + SimpleNamespace( + content=( + "Message sent to session " + "test-platform:FriendMessage:session-1" + ) + ) + ] + ) + ] + llm_resp = SimpleNamespace( + role="assistant", + completion_text="定时任务完成。", + ) + + sent = await cron_manager._send_active_agent_fallback_if_needed( + session=session, + req=req, + llm_resp=llm_resp, + cron_meta={"id": "job-1", "name": "job"}, + ) + + assert sent is False + mock_context.send_message.assert_not_awaited() + + class TestGetNextRunTime: """Tests for _get_next_run_time method."""