From 77b04af9001763b10a63578f6945b2fa81090346 Mon Sep 17 00:00:00 2001 From: seichris Date: Wed, 25 Mar 2026 09:28:21 +0800 Subject: [PATCH] fix(tools): treat Slack channel IDs as explicit send_message targets --- tests/tools/test_send_message_tool.py | 39 +++++++++++++++++++++++++++ tools/send_message_tool.py | 16 ++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index 058678d36a99..69ce834785e3 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -238,6 +238,45 @@ def test_media_only_message_uses_placeholder_for_mirroring(self): thread_id=None, ) + def test_explicit_slack_channel_id_sends_directly_instead_of_home(self): + slack_cfg = SimpleNamespace(enabled=True, token="xoxb-test", extra={}) + config = SimpleNamespace( + platforms={Platform.SLACK: slack_cfg}, + get_home_channel=lambda _platform: None, + ) + + with patch("gateway.config.load_gateway_config", return_value=config), \ + patch("tools.interrupt.is_interrupted", return_value=False), \ + patch("model_tools._run_async", side_effect=_run_async_immediately), \ + patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"success": True})) as send_mock, \ + patch("gateway.mirror.mirror_to_session", return_value=True) as mirror_mock: + result = json.loads( + send_message_tool( + { + "action": "send", + "target": "slack:C0AD2A7C4G1", + "message": "hello", + } + ) + ) + + assert result["success"] is True + send_mock.assert_awaited_once_with( + Platform.SLACK, + slack_cfg, + "C0AD2A7C4G1", + "hello", + thread_id=None, + media_files=[], + ) + mirror_mock.assert_called_once_with( + "slack", + "C0AD2A7C4G1", + "hello", + source_label="cli", + thread_id=None, + ) + class TestSendTelegramMediaDelivery: def test_sends_text_then_photo_for_media_tag(self, tmp_path, monkeypatch): diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index ed0a5cb60e2d..9e96a6cf34d7 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -15,6 +15,8 @@ logger = logging.getLogger(__name__) _TELEGRAM_TOPIC_TARGET_RE = re.compile(r"^\s*(-?\d+)(?::(\d+))?\s*$") +_SLACK_MENTION_RE = re.compile(r"^<\#([CGDU][A-Z0-9]+)(?:\|[^>]+)?>$") +_SLACK_CHANNEL_ID_RE = re.compile(r"^[CGDU][A-Z0-9]+$") _IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"} _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".3gp"} _AUDIO_EXTS = {".ogg", ".opus", ".mp3", ".wav", ".m4a"} @@ -41,7 +43,7 @@ }, "target": { "type": "string", - "description": "Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or Telegram topic 'telegram:chat_id:thread_id'. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:#bot-home', 'slack:#engineering', 'signal:+15551234567'" + "description": "Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or Telegram topic 'telegram:chat_id:thread_id'. Slack also accepts raw channel IDs like 'slack:C0AD2A7C4G1' and Slack mentions like 'slack:#agents-updates'. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567'" }, "message": { "type": "string", @@ -194,6 +196,18 @@ def _handle_send(args): def _parse_target_ref(platform_name: str, target_ref: str): """Parse a tool target into chat_id/thread_id and whether it is explicit.""" + target_ref = target_ref.strip() + + if platform_name == "slack": + mention = _SLACK_MENTION_RE.fullmatch(target_ref) + if mention: + target_ref = mention.group(1) + topic_suffix = re.fullmatch(r"^([CGDU][A-Z0-9]+)\s*/\s*topic\s+\d+$", target_ref, re.IGNORECASE) + if topic_suffix: + target_ref = topic_suffix.group(1) + if _SLACK_CHANNEL_ID_RE.fullmatch(target_ref): + return target_ref, None, True + if platform_name == "telegram": match = _TELEGRAM_TOPIC_TARGET_RE.fullmatch(target_ref) if match: