From 2dcc3f8c843bf712e10e641222ca691268d81e63 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 30 Apr 2026 03:52:34 +0000 Subject: [PATCH] fix: support Slack thread targets in send_message --- tests/tools/test_send_message_tool.py | 7 +++++++ tools/send_message_tool.py | 10 ++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index ff539f63e3f64..92db074eb22ad 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -858,6 +858,12 @@ def test_private_channel_id_is_explicit(self): def test_dm_id_is_explicit(self): assert _parse_target_ref("slack", "D123ABCDEF")[2] is True + def test_channel_id_with_thread_ts_is_explicit(self): + chat_id, thread_id, is_explicit = _parse_target_ref("slack", "C0B0QV5434G:1777520157.319429") + assert chat_id == "C0B0QV5434G" + assert thread_id == "1777520157.319429" + assert is_explicit is True + def test_user_id_is_not_explicit(self): """Slack user IDs (U...) and workspace IDs (W...) are NOT explicit send targets. chat.postMessage rejects them — a DM must be opened first via @@ -875,6 +881,7 @@ def test_lowercase_or_short_id_is_not_explicit(self): assert _parse_target_ref("slack", "c0b0qv5434g")[2] is False assert _parse_target_ref("slack", "C123")[2] is False assert _parse_target_ref("slack", "X0B0QV5434G")[2] is False + assert _parse_target_ref("slack", "C0B0QV5434G:not-a-ts")[2] is False def test_slack_id_not_explicit_for_other_platforms(self): assert _parse_target_ref("discord", "C0B0QV5434G")[2] is False diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index e282f4c269126..9bef5a00f69bd 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -26,7 +26,7 @@ # because the API requires a conversation ID. To DM a user you must first call # conversations.open to obtain a D... ID. Without this gate, Slack IDs fall # through to channel-name resolution, which only matches by name and fails. -_SLACK_TARGET_RE = re.compile(r"^\s*([CGD][A-Z0-9]{8,})\s*$") +_SLACK_TARGET_RE = re.compile(r"^\s*([CGD][A-Z0-9]{8,})(?::([0-9]+(?:\.[0-9]+)?))?\s*$") _WEIXIN_TARGET_RE = re.compile(r"^\s*((?:wxid|gh|v\d+|wm|wb)_[A-Za-z0-9_-]+|[A-Za-z0-9._-]+@chatroom|filehelper)\s*$") _YUANBAO_TARGET_RE = re.compile(r"^\s*((?:group|direct):[^:]+)\s*$") # Discord snowflake IDs are numeric, same regex pattern as Telegram topic targets. @@ -338,7 +338,7 @@ def _parse_target_ref(platform_name: str, target_ref: str): if platform_name == "slack": match = _SLACK_TARGET_RE.fullmatch(target_ref) if match: - return match.group(1), None, True + return match.group(1), match.group(2), True if platform_name == "weixin": match = _WEIXIN_TARGET_RE.fullmatch(target_ref) if match: @@ -589,7 +589,7 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, last_result = None for chunk in chunks: if platform == Platform.SLACK: - result = await _send_slack(pconfig.token, chat_id, chunk) + result = await _send_slack(pconfig.token, chat_id, chunk, thread_id=thread_id) elif platform == Platform.WHATSAPP: result = await _send_whatsapp(pconfig.extra, chat_id, chunk) elif platform == Platform.SIGNAL: @@ -980,7 +980,7 @@ async def _send_discord(token, chat_id, message, thread_id=None, media_files=Non return _error(f"Discord send failed: {e}") -async def _send_slack(token, chat_id, message): +async def _send_slack(token, chat_id, message, thread_id=None): """Send via Slack Web API.""" try: import aiohttp @@ -994,6 +994,8 @@ async def _send_slack(token, chat_id, message): headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30), **_sess_kw) as session: payload = {"channel": chat_id, "text": message, "mrkdwn": True} + if thread_id: + payload["thread_ts"] = str(thread_id) async with session.post(url, headers=headers, json=payload, **_req_kw) as resp: data = await resp.json() if data.get("ok"):