diff --git a/agent/tools/scheduler/integration.py b/agent/tools/scheduler/integration.py index 7421a525..3d6b8f8d 100644 --- a/agent/tools/scheduler/integration.py +++ b/agent/tools/scheduler/integration.py @@ -255,6 +255,12 @@ def _execute_agent_task(task: dict, agent_bridge) -> bool: logger.error(f"[Scheduler] Task {task['id']}: No result from agent execution") return True # agent ran but produced nothing; don't loop + if action.get("silent", False): + logger.info( + f"[Scheduler] Task {task['id']} executed successfully in silent mode" + ) + return True + from channel.channel_factory import create_channel channel = create_channel(channel_type) if not channel: diff --git a/agent/tools/scheduler/scheduler_tool.py b/agent/tools/scheduler/scheduler_tool.py index 8535f1ca..de8802a1 100644 --- a/agent/tools/scheduler/scheduler_tool.py +++ b/agent/tools/scheduler/scheduler_tool.py @@ -64,6 +64,11 @@ class SchedulerTool(BaseTool): "schedule_value": { "type": "string", "description": "调度值: cron表达式/间隔秒数/时间(+5s,+10m,+1h或ISO格式)" + }, + "silent": { + "type": "boolean", + "default": False, + "description": "Silent mode (default false): when true, the task runs normally but its result is not pushed. Set true only when the user explicitly says they don't need the result; reminder, notification and broadcast tasks must keep it false" } }, "required": ["action"] @@ -184,6 +189,9 @@ class SchedulerTool(BaseTool): "channel_type": self.config.get("channel_type", "unknown"), "notify_session_id": notify_session_id, } + # silent only applies to ai_task; fixed messages always deliver + if kwargs.get("silent", False): + action["silent"] = True # 针对钉钉单聊,额外存储 sender_staff_id msg = context.kwargs.get("msg") @@ -216,14 +224,17 @@ class SchedulerTool(BaseTool): content_desc = f"💬 固定消息: {message}" else: content_desc = f"🤖 AI任务: {ai_task}" - + + # Warn the user at creation time so a mistaken silent flag is easy to spot + silent_desc = "\n🔇 静默模式: 执行后不会推送结果" if action.get("silent") else "" + return ( f"✅ 定时任务创建成功\n\n" f"📋 任务ID: {task_id}\n" f"📝 名称: {name}\n" f"⏰ 调度: {schedule_desc}\n" f"👤 接收者: {receiver_desc}\n" - f"{content_desc}\n" + f"{content_desc}{silent_desc}\n" f"🕐 下次执行: {next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else '未知'}" ) diff --git a/tests/test_scheduler_silent.py b/tests/test_scheduler_silent.py new file mode 100644 index 00000000..fabc64be --- /dev/null +++ b/tests/test_scheduler_silent.py @@ -0,0 +1,92 @@ +"""Regression tests for silent scheduled agent tasks.""" + +import os +import sys +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from agent.tools.scheduler.integration import _execute_agent_task +from agent.tools.scheduler.scheduler_tool import SchedulerTool + + +class _Context(dict): + kwargs = {} + + +class _TaskStore: + def __init__(self): + self.added = [] + + def add_task(self, task): + self.added.append(task) + + +class _AgentBridge: + def __init__(self, content="maintenance complete"): + self.content = content + self.calls = [] + + def agent_reply(self, task_description, **kwargs): + self.calls.append((task_description, kwargs)) + return SimpleNamespace(content=self.content) + + +class TestSchedulerSilentMode(unittest.TestCase): + def test_schema_exposes_silent_for_agent_tasks(self): + silent = SchedulerTool.params["properties"]["silent"] + + self.assertEqual(silent["type"], "boolean") + self.assertFalse(silent["default"]) + + def test_create_persists_silent_on_agent_task(self): + tool = SchedulerTool({"channel_type": "web"}) + store = _TaskStore() + tool.task_store = store + tool.current_context = _Context( + receiver="user-1", + session_id="session-1", + isgroup=False, + ) + + result = tool.execute( + { + "action": "create", + "name": "refresh token", + "ai_task": "refresh the token", + "schedule_type": "interval", + "schedule_value": "3000", + "silent": True, + } + ) + + self.assertEqual(result.status, "success") + self.assertEqual(len(store.added), 1) + self.assertIs(store.added[0]["action"]["silent"], True) + + def test_silent_agent_task_executes_without_delivery(self): + bridge = _AgentBridge() + task = { + "id": "task-1", + "action": { + "type": "agent_task", + "task_description": "rotate logs", + "receiver": "user-1", + "is_group": False, + "channel_type": "web", + "silent": True, + }, + } + + with patch("channel.channel_factory.create_channel") as create_channel: + result = _execute_agent_task(task, bridge) + + self.assertTrue(result) + self.assertEqual(len(bridge.calls), 1) + create_channel.assert_not_called() + + +if __name__ == "__main__": + unittest.main()