From 4044b85d4bfa9104b633f3cb408894f0459a0164 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Mon, 9 Mar 2026 01:32:10 +0800 Subject: [PATCH 1/4] fix: ensure feishu audio file has .opus extension for Groq Whisper compatibility --- nanobot/channels/feishu.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index a637025..0409c32 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -753,8 +753,9 @@ class FeishuChannel(BaseChannel): None, self._download_file_sync, message_id, file_key, msg_type ) if not filename: - ext = {"audio": ".opus", "media": ".mp4"}.get(msg_type, "") - filename = f"{file_key[:16]}{ext}" + filename = file_key[:16] + if msg_type == "audio" and not filename.endswith(".opus"): + filename = f"{filename}.opus" if data and filename: file_path = media_dir / filename From 620d7896c710748053257695d25c3391aa637dc5 Mon Sep 17 00:00:00 2001 From: ailuntz Date: Tue, 10 Mar 2026 00:14:34 +0800 Subject: [PATCH 2/4] fix(slack): define thread usage when sending messages --- nanobot/channels/slack.py | 2 +- tests/test_slack_channel.py | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/test_slack_channel.py diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index a4e7324..e36c4c9 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -82,6 +82,7 @@ class SlackChannel(BaseChannel): thread_ts = slack_meta.get("thread_ts") channel_type = slack_meta.get("channel_type") # Only reply in thread for channel/group messages; DMs don't use threads + use_thread = bool(thread_ts and channel_type != "im") thread_ts_param = thread_ts if use_thread else None # Slack rejects empty text payloads. Keep media-only messages media-only, @@ -278,4 +279,3 @@ class SlackChannel(BaseChannel): if parts: rows.append(" ยท ".join(parts)) return "\n".join(rows) - diff --git a/tests/test_slack_channel.py b/tests/test_slack_channel.py new file mode 100644 index 0000000..18b96ef --- /dev/null +++ b/tests/test_slack_channel.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import pytest + +from nanobot.bus.events import OutboundMessage +from nanobot.bus.queue import MessageBus +from nanobot.channels.slack import SlackChannel +from nanobot.config.schema import SlackConfig + + +class _FakeAsyncWebClient: + def __init__(self) -> None: + self.chat_post_calls: list[dict[str, object | None]] = [] + self.file_upload_calls: list[dict[str, object | None]] = [] + + async def chat_postMessage( + self, + *, + channel: str, + text: str, + thread_ts: str | None = None, + ) -> None: + self.chat_post_calls.append( + { + "channel": channel, + "text": text, + "thread_ts": thread_ts, + } + ) + + async def files_upload_v2( + self, + *, + channel: str, + file: str, + thread_ts: str | None = None, + ) -> None: + self.file_upload_calls.append( + { + "channel": channel, + "file": file, + "thread_ts": thread_ts, + } + ) + + +@pytest.mark.asyncio +async def test_send_uses_thread_for_channel_messages() -> None: + channel = SlackChannel(SlackConfig(enabled=True), MessageBus()) + fake_web = _FakeAsyncWebClient() + channel._web_client = fake_web + + await channel.send( + OutboundMessage( + channel="slack", + chat_id="C123", + content="hello", + media=["/tmp/demo.txt"], + metadata={"slack": {"thread_ts": "1700000000.000100", "channel_type": "channel"}}, + ) + ) + + assert len(fake_web.chat_post_calls) == 1 + assert fake_web.chat_post_calls[0]["thread_ts"] == "1700000000.000100" + assert len(fake_web.file_upload_calls) == 1 + assert fake_web.file_upload_calls[0]["thread_ts"] == "1700000000.000100" + + +@pytest.mark.asyncio +async def test_send_omits_thread_for_dm_messages() -> None: + channel = SlackChannel(SlackConfig(enabled=True), MessageBus()) + fake_web = _FakeAsyncWebClient() + channel._web_client = fake_web + + await channel.send( + OutboundMessage( + channel="slack", + chat_id="D123", + content="hello", + media=["/tmp/demo.txt"], + metadata={"slack": {"thread_ts": "1700000000.000100", "channel_type": "im"}}, + ) + ) + + assert len(fake_web.chat_post_calls) == 1 + assert fake_web.chat_post_calls[0]["thread_ts"] is None + assert len(fake_web.file_upload_calls) == 1 + assert fake_web.file_upload_calls[0]["thread_ts"] is None From 28330940d0b2cefbfe740957ee8f51ed9349c24e Mon Sep 17 00:00:00 2001 From: Re-bin Date: Mon, 9 Mar 2026 17:18:10 +0000 Subject: [PATCH 3/4] fix(slack): skip thread_ts for direct messages --- nanobot/channels/slack.py | 5 ++--- tests/test_slack_channel.py | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index e36c4c9..0384d8d 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -81,9 +81,8 @@ class SlackChannel(BaseChannel): slack_meta = msg.metadata.get("slack", {}) if msg.metadata else {} thread_ts = slack_meta.get("thread_ts") channel_type = slack_meta.get("channel_type") - # Only reply in thread for channel/group messages; DMs don't use threads - use_thread = bool(thread_ts and channel_type != "im") - thread_ts_param = thread_ts if use_thread else None + # Slack DMs don't use threads; channel/group replies may keep thread_ts. + thread_ts_param = thread_ts if thread_ts and channel_type != "im" else None # Slack rejects empty text payloads. Keep media-only messages media-only, # but send a single blank message when the bot has no text or files to send. diff --git a/tests/test_slack_channel.py b/tests/test_slack_channel.py index 18b96ef..891f86a 100644 --- a/tests/test_slack_channel.py +++ b/tests/test_slack_channel.py @@ -61,6 +61,7 @@ async def test_send_uses_thread_for_channel_messages() -> None: ) assert len(fake_web.chat_post_calls) == 1 + assert fake_web.chat_post_calls[0]["text"] == "hello\n" assert fake_web.chat_post_calls[0]["thread_ts"] == "1700000000.000100" assert len(fake_web.file_upload_calls) == 1 assert fake_web.file_upload_calls[0]["thread_ts"] == "1700000000.000100" @@ -83,6 +84,7 @@ async def test_send_omits_thread_for_dm_messages() -> None: ) assert len(fake_web.chat_post_calls) == 1 + assert fake_web.chat_post_calls[0]["text"] == "hello\n" assert fake_web.chat_post_calls[0]["thread_ts"] is None assert len(fake_web.file_upload_calls) == 1 assert fake_web.file_upload_calls[0]["thread_ts"] is None From 1284c7217ea2c59a5a9e2786c5f550e9fb5ace1b Mon Sep 17 00:00:00 2001 From: Protocol Zero <257158451+Protocol-zero-0@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:12:11 +0000 Subject: [PATCH 4/4] fix(cli): let gateway use config port by default Respect config.gateway.port when --port is omitted, while keeping CLI flags as the highest-precedence override. --- nanobot/cli/commands.py | 3 ++- tests/test_commands.py | 44 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 2c8d6d3..37f08b2 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -290,7 +290,7 @@ def _load_runtime_config(config: str | None = None, workspace: str | None = None @app.command() def gateway( - port: int = typer.Option(18790, "--port", "-p", help="Gateway port"), + port: int | None = typer.Option(None, "--port", "-p", help="Gateway port"), workspace: str | None = typer.Option(None, "--workspace", "-w", help="Workspace directory"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"), config: str | None = typer.Option(None, "--config", "-c", help="Path to config file"), @@ -310,6 +310,7 @@ def gateway( logging.basicConfig(level=logging.DEBUG) config = _load_runtime_config(config, workspace) + port = port if port is not None else config.gateway.port console.print(f"{__logo__} Starting nanobot gateway on port {port}...") sync_workspace_templates(config.workspace_path) diff --git a/tests/test_commands.py b/tests/test_commands.py index 19c1998..5d38942 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -356,3 +356,47 @@ def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Pat assert isinstance(result.exception, _StopGateway) assert seen["cron_store"] == config_file.parent / "cron" / "jobs.json" + + +def test_gateway_uses_configured_port_when_cli_flag_is_missing(monkeypatch, tmp_path: Path) -> None: + config_file = tmp_path / "instance" / "config.json" + config_file.parent.mkdir(parents=True) + config_file.write_text("{}") + + config = Config() + config.gateway.port = 18791 + + monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) + monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) + monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) + monkeypatch.setattr( + "nanobot.cli.commands._make_provider", + lambda _config: (_ for _ in ()).throw(_StopGateway("stop")), + ) + + result = runner.invoke(app, ["gateway", "--config", str(config_file)]) + + assert isinstance(result.exception, _StopGateway) + assert "port 18791" in result.stdout + + +def test_gateway_cli_port_overrides_configured_port(monkeypatch, tmp_path: Path) -> None: + config_file = tmp_path / "instance" / "config.json" + config_file.parent.mkdir(parents=True) + config_file.write_text("{}") + + config = Config() + config.gateway.port = 18791 + + monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) + monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) + monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) + monkeypatch.setattr( + "nanobot.cli.commands._make_provider", + lambda _config: (_ for _ in ()).throw(_StopGateway("stop")), + ) + + result = runner.invoke(app, ["gateway", "--config", str(config_file), "--port", "18792"]) + + assert isinstance(result.exception, _StopGateway) + assert "port 18792" in result.stdout