diff --git a/gateway/config.py b/gateway/config.py index 45c9e5ce1..af2007c9e 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -333,6 +333,29 @@ PORT_BINDING_PLATFORM_VALUES = frozenset({ "line", }) +# Platforms whose port-binding status depends on connection mode. Feishu in +# websocket mode (its default) uses an outbound long connection — no listener. +# Only webhook/callback mode binds a port. Maps platform value → the mode +# value that actually binds (#52563). +PORT_BINDING_CONDITIONAL_MODES: dict[str, str] = { + "feishu": "webhook", +} + + +def platform_binds_port(platform_value: str, extra: Optional[dict] = None) -> bool: + """Return True when *platform_value* actually binds a port for *extra* config. + + Mode-conditional platforms (Feishu) only bind in their listener mode; + everything else in ``PORT_BINDING_PLATFORM_VALUES`` always binds. + """ + if platform_value not in PORT_BINDING_PLATFORM_VALUES: + return False + expected_mode = PORT_BINDING_CONDITIONAL_MODES.get(platform_value) + if expected_mode is not None: + actual = str((extra or {}).get("connection_mode", "websocket")).strip().lower() + return actual == expected_mode + return True + @dataclass class HomeChannel: diff --git a/gateway/run.py b/gateway/run.py index a73f0924c..751a9ace8 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1417,6 +1417,7 @@ from contextlib import contextmanager as _contextmanager # dashboard's pre-write validation enforces the same policy. from gateway.config import ( PORT_BINDING_PLATFORM_VALUES as _PORT_BINDING_PLATFORM_VALUES, + platform_binds_port as _platform_binds_port, ) @@ -8802,7 +8803,8 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew port_binding_platforms = sorted( platform.value for platform, platform_config in profile_cfg.platforms.items() - if platform_config.enabled and platform.value in _PORT_BINDING_PLATFORM_VALUES + if platform_config.enabled + and _platform_binds_port(platform.value, platform_config.extra) ) if port_binding_platforms: joined = ", ".join(port_binding_platforms) diff --git a/tests/gateway/test_multiplex_adapter_registry.py b/tests/gateway/test_multiplex_adapter_registry.py index d2c324c50..246d8602d 100644 --- a/tests/gateway/test_multiplex_adapter_registry.py +++ b/tests/gateway/test_multiplex_adapter_registry.py @@ -530,3 +530,72 @@ class TestSecondaryProfileConfigHandling: "line", ): assert p in _PORT_BINDING_PLATFORM_VALUES + + + +class TestFeishuPortBindingConditional: + """Feishu websocket mode does NOT bind a port; only webhook mode does (#52563).""" + + @pytest.mark.asyncio + async def test_feishu_websocket_mode_not_rejected(self, monkeypatch): + """Feishu in websocket mode (the default) should NOT raise MultiplexConfigError.""" + from gateway.run import MultiplexConfigError + from gateway.config import GatewayConfig, Platform, PlatformConfig + + runner = GatewayRunner.__new__(GatewayRunner) + runner.config = GatewayConfig(multiplex_profiles=True) + runner._profile_adapters = {} + + reviewer_cfg = GatewayConfig(multiplex_profiles=True) + reviewer_cfg.platforms = { + Platform.FEISHU: PlatformConfig( + enabled=True, + extra={"app_id": "cli_xxx", "app_secret": "sec", "connection_mode": "websocket"}, + ), + } + monkeypatch.setattr("gateway.config.load_gateway_config", lambda: reviewer_cfg) + monkeypatch.setattr(runner, "_create_adapter", lambda p, c: None) + + connected = await runner._start_one_profile_adapters("reviewer", "/tmp/x", {}) + assert connected == 0 # no error, just nothing connected + + @pytest.mark.asyncio + async def test_feishu_webhook_mode_raises(self, monkeypatch): + """Feishu in webhook mode binds a port and should raise MultiplexConfigError.""" + from gateway.run import MultiplexConfigError + from gateway.config import GatewayConfig, Platform, PlatformConfig + + runner = GatewayRunner.__new__(GatewayRunner) + runner.config = GatewayConfig(multiplex_profiles=True) + runner._profile_adapters = {} + + reviewer_cfg = GatewayConfig(multiplex_profiles=True) + reviewer_cfg.platforms = { + Platform.FEISHU: PlatformConfig( + enabled=True, + extra={"app_id": "cli_xxx", "app_secret": "sec", "connection_mode": "webhook"}, + ), + } + monkeypatch.setattr("gateway.config.load_gateway_config", lambda: reviewer_cfg) + + with pytest.raises(MultiplexConfigError) as ei: + await runner._start_one_profile_adapters("reviewer", "/tmp/x", {}) + assert "feishu" in str(ei.value) + + def test_platform_binds_port_helper(self): + """Unit test for _platform_binds_port helper.""" + from gateway.run import _platform_binds_port + + # Non-port-binding platform + assert _platform_binds_port("telegram", {}) is False + + # Unconditional port-binding platform + assert _platform_binds_port("webhook", {}) is True + assert _platform_binds_port("api_server", {}) is True + + # Feishu: websocket = no port binding + assert _platform_binds_port("feishu", {"connection_mode": "websocket"}) is False + assert _platform_binds_port("feishu", {}) is False # default is websocket + + # Feishu: webhook = port binding + assert _platform_binds_port("feishu", {"connection_mode": "webhook"}) is True