Reflect IM channel runtime health

This commit is contained in:
taohe
2026-06-11 17:11:55 +08:00
parent dae7c7870e
commit b26b30ac3d
4 changed files with 81 additions and 0 deletions
+7
View File
@@ -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
@@ -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
@@ -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
+10
View File
@@ -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"}