mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(slack): preserve media in standalone cron delivery
This commit is contained in:
parent
f279f7fcf1
commit
4f726ed467
4 changed files with 162 additions and 14 deletions
|
|
@ -6507,6 +6507,58 @@ async def _standalone_send(
|
|||
exc_info=True,
|
||||
)
|
||||
|
||||
# Out-of-process cron runs have no live SlackAdapter. Upload MEDIA files
|
||||
# here so the standalone path has feature parity with the live adapter
|
||||
# instead of silently succeeding with text only. This runs BEFORE the
|
||||
# empty-text skip: a media delivery with a blank caption must still
|
||||
# upload the files.
|
||||
if media_files:
|
||||
if not check_slack_requirements():
|
||||
return {"error": "Slack send failed: slack-sdk is not installed"}
|
||||
|
||||
paths = []
|
||||
for media in media_files:
|
||||
path = media[0] if isinstance(media, (tuple, list)) else media
|
||||
path = str(path)
|
||||
if not os.path.isfile(path):
|
||||
return {"error": f"Slack media file not found: {os.path.basename(path)}"}
|
||||
paths.append(path)
|
||||
|
||||
try:
|
||||
client = AsyncWebClient(token=token) # pyright: ignore[reportCallIssue]
|
||||
_apply_slack_proxy(client, resolve_proxy_url())
|
||||
upload_result = None
|
||||
for start in range(0, len(paths), 10):
|
||||
batch = paths[start : start + 10]
|
||||
kwargs = {
|
||||
"channel": chat_id,
|
||||
"initial_comment": formatted if start == 0 else "",
|
||||
"thread_ts": thread_id,
|
||||
}
|
||||
if len(batch) == 1:
|
||||
kwargs.update(
|
||||
file=batch[0],
|
||||
filename=os.path.basename(batch[0]),
|
||||
)
|
||||
else:
|
||||
kwargs["file_uploads"] = [
|
||||
{"file": path, "filename": os.path.basename(path)}
|
||||
for path in batch
|
||||
]
|
||||
upload_result = await client.files_upload_v2(**kwargs)
|
||||
return {
|
||||
"success": True,
|
||||
"platform": "slack",
|
||||
"chat_id": chat_id,
|
||||
"message_id": (
|
||||
upload_result.get("ts")
|
||||
if upload_result is not None and hasattr(upload_result, "get")
|
||||
else None
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Slack media upload failed: {e}"}
|
||||
|
||||
if not formatted or not formatted.strip():
|
||||
logger.debug("[Slack] _standalone_send: skipping empty/whitespace message")
|
||||
return {
|
||||
|
|
@ -6521,7 +6573,7 @@ async def _standalone_send(
|
|||
return {"error": "aiohttp not installed. Run: pip install aiohttp"}
|
||||
|
||||
try:
|
||||
from gateway.platforms.base import resolve_proxy_url, proxy_kwargs_for_aiohttp
|
||||
from gateway.platforms.base import proxy_kwargs_for_aiohttp
|
||||
|
||||
_proxy = resolve_proxy_url()
|
||||
_sess_kw, _req_kw = proxy_kwargs_for_aiohttp(_proxy)
|
||||
|
|
|
|||
|
|
@ -1207,6 +1207,50 @@ class TestSlackProxyBehavior:
|
|||
assert adapter._handler.client.proxy is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestStandaloneSendMedia
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestStandaloneSendMedia:
|
||||
@pytest.mark.asyncio
|
||||
async def test_uploads_local_media_with_message_as_caption(self, tmp_path):
|
||||
"""Standalone cron sends should use files_upload_v2, not omit the image."""
|
||||
image = tmp_path / "daily-report.png"
|
||||
image.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
client = MagicMock()
|
||||
client.files_upload_v2 = AsyncMock(
|
||||
return_value={"ok": True, "files": [{"id": "F123"}]}
|
||||
)
|
||||
config = PlatformConfig(enabled=True, token="xoxb-fake-token")
|
||||
|
||||
with (
|
||||
patch.object(_slack_mod, "AsyncWebClient", return_value=client),
|
||||
patch.object(_slack_mod, "resolve_proxy_url", return_value=None),
|
||||
patch.object(
|
||||
_slack_mod.aiohttp,
|
||||
"ClientSession",
|
||||
side_effect=AssertionError("media delivery used text-only chat.postMessage"),
|
||||
),
|
||||
):
|
||||
result = await _slack_mod._standalone_send(
|
||||
config,
|
||||
"C123",
|
||||
"daily report",
|
||||
thread_id=None,
|
||||
media_files=[(str(image), False)],
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
client.files_upload_v2.assert_awaited_once_with(
|
||||
channel="C123",
|
||||
file=str(image),
|
||||
filename="daily-report.png",
|
||||
initial_comment="daily report",
|
||||
thread_ts=None,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestSendDocument
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -755,6 +755,44 @@ class TestSendToPlatformChunking:
|
|||
thread_ts=None,
|
||||
)
|
||||
|
||||
def test_slack_media_is_forwarded_to_standalone_plugin(self, monkeypatch, tmp_path):
|
||||
"""Out-of-process cron delivery must not silently drop Slack MEDIA files."""
|
||||
_ensure_slack_mock(monkeypatch)
|
||||
media_path = tmp_path / "daily-report.png"
|
||||
media_path.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
media_files = [(str(media_path), False)]
|
||||
pconfig = SimpleNamespace(enabled=True, token="***", extra={})
|
||||
|
||||
entry = _slack_entry()
|
||||
assert entry is not None
|
||||
original = entry.standalone_sender_fn
|
||||
send = AsyncMock(
|
||||
return_value={"success": True, "platform": "slack", "message_id": "1"}
|
||||
)
|
||||
entry.standalone_sender_fn = send
|
||||
try:
|
||||
result = asyncio.run(
|
||||
_send_to_platform(
|
||||
Platform.SLACK,
|
||||
pconfig,
|
||||
"C123",
|
||||
"daily report",
|
||||
media_files=media_files,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
entry.standalone_sender_fn = original
|
||||
|
||||
assert result["success"] is True
|
||||
send.assert_awaited_once_with(
|
||||
pconfig,
|
||||
"C123",
|
||||
"daily report",
|
||||
thread_id=None,
|
||||
media_files=media_files,
|
||||
force_document=False,
|
||||
)
|
||||
|
||||
def test_slack_bold_italic_formatted_before_send(self, monkeypatch):
|
||||
"""Bold+italic ***text*** survives tool-layer formatting."""
|
||||
_ensure_slack_mock(monkeypatch)
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,32 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
|
|||
last_result = result
|
||||
return last_result
|
||||
|
||||
# --- Slack: route both text and native files through the plugin's
|
||||
# standalone sender. This path is used by out-of-process cron runs where
|
||||
# no live gateway adapter is available; dropping ``media_files`` here made
|
||||
# MEDIA directives disappear while the text delivery still reported
|
||||
# success.
|
||||
if platform == Platform.SLACK:
|
||||
from gateway.platform_registry import platform_registry
|
||||
entry = platform_registry.get("slack")
|
||||
if entry is None or entry.standalone_sender_fn is None:
|
||||
return {"error": "Slack plugin not registered or missing standalone_sender_fn"}
|
||||
last_result = None
|
||||
for i, chunk in enumerate(chunks):
|
||||
is_last = i == len(chunks) - 1
|
||||
result = await entry.standalone_sender_fn(
|
||||
pconfig,
|
||||
chat_id,
|
||||
chunk,
|
||||
thread_id=thread_id,
|
||||
media_files=media_files if is_last else [],
|
||||
force_document=force_document,
|
||||
)
|
||||
if isinstance(result, dict) and result.get("error"):
|
||||
return result
|
||||
last_result = result
|
||||
return last_result
|
||||
|
||||
# --- Non-media platforms ---
|
||||
if media_files and not message.strip():
|
||||
return {
|
||||
|
|
@ -1049,19 +1075,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:
|
||||
# Slack migrated to a bundled plugin (#41112); delivery flows
|
||||
# through the registry's standalone_sender_fn, which applies
|
||||
# mrkdwn formatting and posts via the Slack Web API.
|
||||
from gateway.platform_registry import platform_registry
|
||||
_slack_entry = platform_registry.get("slack")
|
||||
if _slack_entry is None or _slack_entry.standalone_sender_fn is None:
|
||||
result = {"error": "Slack plugin not registered or missing standalone_sender_fn"}
|
||||
else:
|
||||
result = await _slack_entry.standalone_sender_fn(
|
||||
pconfig, chat_id, chunk, thread_id=thread_id
|
||||
)
|
||||
elif platform == Platform.WHATSAPP:
|
||||
if platform == Platform.WHATSAPP:
|
||||
result = await _registry_standalone_send("whatsapp", pconfig, chat_id, chunk, thread_id)
|
||||
elif platform == Platform.SIGNAL:
|
||||
result = await _send_signal(pconfig.extra, chat_id, chunk)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue