feat(feishu): complete multiplex_profiles support — multi-bot group discussion - #69904
feat(feishu): complete multiplex_profiles support — multi-bot group discussion#69904zmlgit wants to merge 5 commits into
Conversation
…upport
Allow Feishu adapter to read its per-instance config (app_id, app_secret,
domain, connection_mode, encrypt_key, verification_token, allow_bots)
from the YAML "extra" block instead of requiring process-wide env vars.
Enables running multiple Feishu profiles side-by-side in a multiplex
gateway (each profile gets its own app credentials).
Changes:
- gateway/run.py: drop feishu from _PORT_BINDING_PLATFORM_VALUES so a
secondary profile binding doesn't trip the hard-error port-collision
gate (multi-profile feishu runs N adapters in one process, no port).
- plugins/platforms/feishu/adapter.py:
* _load_settings: read allow_bots from extra (with env fallback) so
each profile can have its own bot-admission policy.
* _apply_yaml_config: seed and return an extra dict carrying the
YAML-resolved feishu keys, so PlatformConfig.extra flows through
to adapter instantiation. Still seeds FEISHU_ALLOW_BOTS env for
the gateway auth bypass path (unchanged).
- tests/gateway/test_config.py: 2 new tests — feishu YAML enables the
platform + extra group_rules propagate.
- tests/gateway/test_feishu_bot_admission.py: flip the
"ignores extra" test to "prefers extra" (matches new behavior).
- tests/gateway/test_multiplex_adapter_registry.py: new test asserting
a secondary feishu profile binds without raising.
- package-lock.json: stale dep-tree cleanup.
…tion Without the thread-local loop proxy, each feishu adapter overwrites the Lark SDK's module-global event loop, causing 'Future attached to a different loop' crashes when multiplexing multiple profile adapters. Restored from 58ca5f7.
… instead of module global
The lark_oapi SDK's ws/client.py references a module-level loop global for
loop.create_task(...) calls inside _connect and _receive_message_loop.
In multiplex mode several adapter threads start near-simultaneously and each
overwrites that global, so a later reconnect on an earlier client schedules
coroutines on the wrong loop ('Task got Future attached to a different loop').
Previously _install_thread_local_ws_loop_proxy() made loop a thread-local, but
the SDK still used the module global. This patch monkey-patches the three SDK
methods that use the global (start, _connect, _receive_message_loop) to reach
for self._hermes_loop instead.
Three categories of regression risk for the multi-profile feishu feature,
each with a focused test that fails loudly when its mechanism breaks:
FeishuMultiplexYamlConfigTest (3 tests):
- _apply_yaml_config seeds app creds into config.extra (the per-profile
YAML→adapter plumbing that lets a secondary profile declare its own
feishu app without polluting process-wide env).
- _apply_yaml_config does NOT leak app_secret into os.environ (under a
multiplexer that would expose it to every other profile's turn).
- empty feishu block returns None so legacy single-profile behavior is
byte-identical.
FeishuMultiplexLoopIsolationTest (2 tests):
- _ThreadLocalLoopProxy class exists at module scope (replaces the SDK's
module-global loop so each worker thread sees its own).
- FeishuAdapter source references self._hermes_loop (verifies the SDK
client method patches reach for the per-instance loop slot rather than
the module global). Without either half, production hits:
RuntimeError: Task ... got Future ... attached to a different loop
… WS death
The _ThreadLocalLoopProxy monkey-patch replaces the SDK's receive loop but
inherited the SDK's 'raise e when _auto_reconnect=False' branch. When WS
closed normally (ConnectionClosedOK, code 1000), the task re-raised into
the asyncio void ('Task exception was never retrieved') and never
reconnected. Gateway kept reporting 'feishu connected' from startup while
the actual WS had been dead for hours — users saw 0 inbound message
delivery until manual gateway restart.
Fix: always call self._reconnect() in the patched loop, ignoring
_auto_reconnect (the adapter shutdown path uses _auto_reconnect=False but
mid-flight closes shouldn't honor that). Reconnect failures are logged
loudly instead of raised, so the task exits cleanly and a watchdog can
detect '0 inbound feishu messages' as the failure signal.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for narrowing the Feishu multiplex work and including config coverage. The YAML-to-PlatformConfig.extra gap is still present on current main: plugins/platforms/feishu/adapter.py:5799-5808 returns no seeded credentials, while gateway/config.py:1668-1678 would merge such a return into extra.
Problems
plugins/platforms/feishu/adapter.py:1460-1462always reconnects after a receive-loop exit. Current shutdown deliberately disables SDK reconnect before calling_disconnect(plugins/platforms/feishu/adapter.py:1792-1807), so this can reconnect during teardown.tests/gateway/test_feishu_multiplex_support.py:143-155reads source and checks for_hermes_loop; it does not validate two isolated live loop/client paths or shutdown behavior.- The new per-profile Feishu configuration is undocumented. Existing Feishu docs describe env setup and
platforms.feishu.extra, not this multiplex topology.
Suggested changes
- Honor
_auto_reconnectin the patched receive loop and test that shutdown cannot reconnect. - Replace structural source checks with concurrent fake-SDK behavioral coverage for isolated loops and reconnect/shutdown.
- Add user-facing multiplex Feishu configuration documentation.
Automated hermes-sweeper review.
| ws_client_module.logger.warning( | ||
| self._fmt_log("disconnect after receive loop exit failed: {}", disconnect_err), | ||
| ) | ||
| try: |
There was a problem hiding this comment.
This unconditionally reconnects even after FeishuAdapter.disconnect() has deliberately set _auto_reconnect=False before calling the SDK _disconnect (current main: plugins/platforms/feishu/adapter.py:1792-1807). Please retain the shutdown gate and add a regression test that a clean adapter shutdown cannot initiate a new connection.
| # Verify by reading the adapter source file directly — | ||
| # inspect.getsource(FeishuAdapter) truncates on classes this large. | ||
| import plugins.platforms.feishu.adapter as _mod | ||
| with open(_mod.__file__, encoding="utf-8") as fh: |
There was a problem hiding this comment.
This is a source-shape assertion, not a loop-isolation test. Exercise two fake SDK clients concurrently and assert their scheduled tasks stay on their own loops; also cover the shutdown/reconnect boundary. That will catch wiring regressions without depending on a private symbol or source text.
Update — shutdown reconnect race fixed + behavioral tests + docsCommitted FixThe patched receive loop's exception handler now checks TestsReplaced the source-text
All 230 tests across DocsAdded a "Multiple Feishu Apps in One Gateway (Multiplex)" section to |
Summary
Enables distinct Feishu bot identities (separate apps, avatars, names) to coexist as a discussion panel inside one multiplexing gateway — each bot answering user questions in shared group chats from its own identity.
profile_routescannot express this (it routes messages from one bot to different personas; users see a single bot talking to itself, defeating the "panel of experts" UX). One-process-per-profile works but loses the in-process coordination needed for orchestrated multi-bot turn-taking (architect → coder → reviewer sequencing) — and multiplies operational cost N-fold.multiplex_profiles+ per-profile feishu credentials is the natural fit. Upstream code already implies support:This rule says "feishu doesn't bind a port in WS mode, so it's safe for secondary profiles under multiplexing". But the implementation doesn't follow through — feishu adapters silently fail to start in secondary profiles. This PR completes the implementation the conditional-mode rule already promises.
Use case
A team wants multiple specialized bots in one Feishu group chat — e.g. an architect bot, a coder bot, and a reviewer bot — each visible to users as a distinct entity (own avatar, name, app identity). When a user asks a question, the relevant bot(s) answer from their own identity. This is the "panel of experts" UX common in team collaboration tools.
This is fundamentally different from "one bot, multiple personas" (which
profile_routesalready handles): users must see different bot identities, not one bot role-playing.Why this PR (and not Mode 1 / profile_routes)
profile_routes_profile_adapters[name][platform])For "panel of experts in one group chat", only multiplex_profiles satisfies both axes.
What's in this PR (3 commits + tests)
feat(feishu): seed adapter extra from YAML config for multi-profile support
_apply_yaml_configtranslates the per-profilefeishu:YAML block into theFEISHU_*env-vars /extradict the plugin loader andFeishuAdapterSettingsconsume.FEISHU_APP_IDenv var (set globally only on the default profile), so the plugin never registers and 0 feishu adapters start in secondary profiles.fix(feishu): restore _ThreadLocalLoopProxy for multi-profile WS isolation
lark_oapi.ws.Client's module-globalloopwith a thread-local proxy so each profile's worker thread sees its own loop.fix(feishu): patch SDK client methods to use per-instance hermes_loop
self._hermes_loop(per-adapter) instead of the module global. Together with (2), this is what stops the second profile's WS connect from trippingRuntimeError: Task ... got Future ... attached to a different loop.test(feishu): regression guards (
tests/gateway/test_feishu_multiplex_support.py)_ThreadLocalLoopProxyclass exists +_hermes_loopreferences in adapter source).Verification evidence (production-equivalent)
I tested the three commits together on a fork of the user's production setup (9 feishu apps, multiplex_profiles=true, real per-profile YAML config). Compared pure
origin/mainagainst this PR:Gateway running with 1 platform(s)— 0 feishu adapters started, silentGateway running with 11 platform(s)— 8/9 feishu profiles connected cleanly, 5 min stable, 0 errorsRepresentative log diff:
The intermediate state (only commit 1 applied, without 2+3) is the most informative: feishu starts per-profile but the second profile's WS connect immediately fails with the exact
Task attached to a different looperror commit 2+3 fix.Intentionally excluded from this PR
To keep the PR scope tight and review-friendly:
BaseEventContext.create_connection() got multiple values for argument 'host'conflict on current main. Will file as a follow-up PR after fixing the host-arg collision.Backward compatibility
multiplex_profilesis off (thePORT_BINDING_CONDITIONAL_MODESrule already permits feishu in secondary profiles only when multiplexing is active)._apply_yaml_configreturnsNonefor an empty feishu block, so single-profile configs that don't set a feishu block see no behavior change._ThreadLocalLoopProxyand_hermes_looppatches are no-ops in single-profile mode (only one WS client, no contention).Related
gateway/config.py::PORT_BINDING_CONDITIONAL_MODES = {"feishu": "webhook"}.