diff --git a/backend/app/channels/feishu.py b/backend/app/channels/feishu.py index 094d24f58..1a2321eb7 100644 --- a/backend/app/channels/feishu.py +++ b/backend/app/channels/feishu.py @@ -88,6 +88,12 @@ class FeishuChannel(Channel): def supports_streaming(self) -> bool: return True + @property + def is_running(self) -> bool: + if not self._running: + return False + return self._thread is not None and self._thread.is_alive() + async def start(self) -> None: if self._running: return @@ -193,6 +199,7 @@ class FeishuChannel(Channel): except Exception: if self._running: logger.exception("Feishu WebSocket error") + self._running = False async def stop(self) -> None: self._running = False diff --git a/backend/app/gateway/routers/channel_connections.py b/backend/app/gateway/routers/channel_connections.py index 260a94b5d..50f2f8d76 100644 --- a/backend/app/gateway/routers/channel_connections.py +++ b/backend/app/gateway/routers/channel_connections.py @@ -214,6 +214,36 @@ def _runtime_unavailable_reason(provider: str) -> str: return f"Enter the required {display_name} credentials to connect this channel." +def _runtime_not_running_reason(provider: str) -> str: + meta = _PROVIDER_META.get(provider) + display_name = meta["display_name"] if meta else provider + return f"{display_name} channel is configured but is not running. Check the credentials and save this channel again." + + +def _runtime_channel_running(provider: str) -> bool | None: + try: + from app.channels.service import get_channel_service + except Exception: + logger.debug("Unable to inspect channel service status", exc_info=True) + return None + + service = get_channel_service() + if service is None: + return None + try: + status = service.get_status() + except Exception: + logger.debug("Unable to read channel service status", exc_info=True) + return None + + if not status.get("service_running"): + return False + channel_status = status.get("channels", {}).get(provider) + if not isinstance(channel_status, dict): + return None + return bool(channel_status.get("running")) + + def _provider_unavailable_reason( config: ChannelConnectionsConfig, channels_config: dict[str, Any], @@ -226,6 +256,8 @@ def _provider_unavailable_reason( return _runtime_unavailable_reason(provider) if not _runtime_channel_configured(provider, channels_config): return _runtime_unavailable_reason(provider) + if _runtime_channel_running(provider) is False: + return _runtime_not_running_reason(provider) return None diff --git a/backend/tests/test_channel_connections_router.py b/backend/tests/test_channel_connections_router.py index 23ff0afd9..305f7bdea 100644 --- a/backend/tests/test_channel_connections_router.py +++ b/backend/tests/test_channel_connections_router.py @@ -2,6 +2,7 @@ from __future__ import annotations +from types import SimpleNamespace from uuid import UUID from _router_auth_helpers import make_authed_test_app @@ -201,6 +202,37 @@ def test_get_providers_reports_unconfigured_when_runtime_channel_is_missing(tmp_ anyio.run(repo.close) +def test_get_providers_reports_configured_channel_not_running(tmp_path, monkeypatch): + import anyio + + repo = anyio.run(_make_repo, tmp_path) + app = _make_app(_enabled_connections_config(), repo, _channels_config()) + service = SimpleNamespace( + get_status=lambda: { + "service_running": True, + "channels": { + "feishu": { + "enabled": True, + "running": False, + } + }, + } + ) + monkeypatch.setattr("app.channels.service.get_channel_service", lambda: service) + + with TestClient(app) as client: + response = client.get("/api/channels/providers") + + assert response.status_code == 200 + by_provider = {item["provider"]: item for item in response.json()["providers"]} + assert by_provider["feishu"]["configured"] is True + assert by_provider["feishu"]["connectable"] is False + assert by_provider["feishu"]["connection_status"] == "not_connected" + assert "configured but is not running" in by_provider["feishu"]["unavailable_reason"] + + anyio.run(repo.close) + + def test_get_providers_uses_newest_connection_status_per_provider(tmp_path): import anyio diff --git a/backend/tests/test_feishu_parser.py b/backend/tests/test_feishu_parser.py index 5ecfb9e0b..da9f780ac 100644 --- a/backend/tests/test_feishu_parser.py +++ b/backend/tests/test_feishu_parser.py @@ -73,6 +73,16 @@ def test_feishu_on_message_plain_text(): assert mock_make_inbound.call_args[1]["text"] == "Hello world" +def test_feishu_is_not_running_when_ws_thread_exits(): + bus = MessageBus() + channel = FeishuChannel(bus, {"app_id": "test", "app_secret": "test"}) + channel._running = True + channel._thread = MagicMock() + channel._thread.is_alive.return_value = False + + assert channel.is_running is False + + def test_feishu_on_message_rich_text(): bus = MessageBus() config = {"app_id": "test", "app_secret": "test"}