feat(gateway): add configurable Feishu websocket reconnect timing

Allow users to configure websocket reconnect behavior via platform extra
config to reduce reconnect latency in production environments.

The official Feishu SDK defaults to:
- First reconnect: random jitter 0-30 seconds
- Subsequent retries: 120 second intervals

This can cause 20-30 second delays before reconnection after network
interruptions. This commit makes these values configurable while keeping
the SDK defaults for backward compatibility.

Configuration via ~/.hermes/config.yaml:
```yaml
platforms:
  feishu:
    extra:
      ws_reconnect_nonce: 0        # Disable first-reconnect jitter (default: 30)
      ws_reconnect_interval: 3     # Retry every 3 seconds (default: 120)
```

Invalid values (negative numbers, non-integers) fall back to SDK defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtuki 2026-04-05 22:45:49 +08:00 committed by Teknium
parent 7cf4bd06bf
commit 7d0bf15121
2 changed files with 55 additions and 2 deletions

View file

@ -270,6 +270,8 @@ class FeishuAdapterSettings:
webhook_host: str
webhook_port: int
webhook_path: str
ws_reconnect_nonce: int = 30
ws_reconnect_interval: int = 120
@dataclass
@ -358,6 +360,22 @@ def _strip_markdown_to_plain_text(text: str) -> str:
return plain.strip()
def _coerce_non_negative_int(value: Any, default: int) -> int:
try:
parsed = int(value)
except (TypeError, ValueError):
return default
return parsed if parsed >= 0 else default
def _coerce_positive_int(value: Any, default: int) -> int:
try:
parsed = int(value)
except (TypeError, ValueError):
return default
return parsed if parsed >= 1 else default
# ---------------------------------------------------------------------------
# Post payload builders and parsers
# ---------------------------------------------------------------------------
@ -1040,6 +1058,8 @@ class FeishuAdapter(BasePlatformAdapter):
str(extra.get("webhook_path") or os.getenv("FEISHU_WEBHOOK_PATH", _DEFAULT_WEBHOOK_PATH)).strip()
or _DEFAULT_WEBHOOK_PATH
),
ws_reconnect_nonce=_coerce_non_negative_int(extra.get("ws_reconnect_nonce"), 30),
ws_reconnect_interval=_coerce_positive_int(extra.get("ws_reconnect_interval"), 120),
)
def _apply_settings(self, settings: FeishuAdapterSettings) -> None:
@ -1062,6 +1082,8 @@ class FeishuAdapter(BasePlatformAdapter):
self._webhook_host = settings.webhook_host
self._webhook_port = settings.webhook_port
self._webhook_path = settings.webhook_path
self._ws_reconnect_nonce = settings.ws_reconnect_nonce
self._ws_reconnect_interval = settings.ws_reconnect_interval
def _build_event_handler(self) -> Any:
if EventDispatcherHandler is None:
@ -3032,6 +3054,11 @@ class FeishuAdapter(BasePlatformAdapter):
event_handler=self._event_handler,
domain=domain,
)
try:
setattr(self._ws_client, "_reconnect_nonce", self._ws_reconnect_nonce)
setattr(self._ws_client, "_reconnect_interval", self._ws_reconnect_interval)
except Exception:
logger.debug("[Feishu] Failed to override websocket reconnect settings", exc_info=True)
self._ws_future = loop.run_in_executor(
None,
_run_official_feishu_ws_client,

View file

@ -283,7 +283,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase):
from gateway.platforms.feishu import FeishuAdapter
adapter = FeishuAdapter(PlatformConfig())
ws_client = object()
ws_client = SimpleNamespace()
with (
patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True),
@ -367,7 +367,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase):
from gateway.platforms.feishu import FeishuAdapter
adapter = FeishuAdapter(PlatformConfig())
ws_client = object()
ws_client = SimpleNamespace()
sleeps = []
with (
@ -561,6 +561,32 @@ class TestAdapterModule(unittest.TestCase):
self.assertIn("register_p2_im_message_reaction_deleted_v1", source)
self.assertIn("register_p2_card_action_trigger", source)
def test_load_settings_uses_sdk_defaults_for_invalid_ws_reconnect_values(self):
from gateway.platforms.feishu import FeishuAdapter
settings = FeishuAdapter._load_settings(
{
"ws_reconnect_nonce": -1,
"ws_reconnect_interval": "bad",
}
)
self.assertEqual(settings.ws_reconnect_nonce, 30)
self.assertEqual(settings.ws_reconnect_interval, 120)
def test_load_settings_accepts_custom_ws_reconnect_values(self):
from gateway.platforms.feishu import FeishuAdapter
settings = FeishuAdapter._load_settings(
{
"ws_reconnect_nonce": 0,
"ws_reconnect_interval": 3,
}
)
self.assertEqual(settings.ws_reconnect_nonce, 0)
self.assertEqual(settings.ws_reconnect_interval, 3)
class TestAdapterBehavior(unittest.TestCase):
@patch.dict(os.environ, {}, clear=True)