Feature/multiple feishu bot support#2404
Open
MarkHoch wants to merge 9 commits intobytedance:mainfrom
Open
Conversation
…roper event loop isolation ## What This Fix Achieves ✅ **Multiple Feishu Bots Can Now Run Simultaneously** - Previously, only one Feishu bot could connect; now you can configure and run multiple bots in the same deer-flow instance ✅ **No More "Event loop is running" Errors** - Fixed the "RuntimeError: This event loop is already running" that occurred when starting multiple Feishu channels ✅ **Proper Event Loop Isolation** - Each Feishu channel gets its own dedicated asyncio event loop, completely isolated from the main uvloop and other channels ✅ **Race Condition Prevention** - Global lock ensures Feishu channels initialize sequentially, preventing conflicts when patching the lark_oapi SDK ✅ **Clean Module Import Handling** - Completely unloads cached lark_oapi modules before importing in each channel's thread, ensuring fresh initialization ✅ **Multi-Instance Configuration Support** - Configure multiple bots with custom names in config.yaml using list format ## Technical Details The root issue was that "lark_oapi.ws.client" captures a module-level event loop at import time. When uvicorn uses uvloop, this captured the main thread's already-running loop. Multiple channels starting simultaneously caused race conditions. The fix: 1. Completely unload any cached lark_oapi modules before importing 2. Only import lark_oapi inside the dedicated WebSocket thread with a fresh event loop 3. Use a global lock to ensure sequential initialization 4. Support multiple named bot instances in configuration ## Configuration Example
Contributor
There was a problem hiding this comment.
Pull request overview
Adds support for running multiple Feishu (and other) channel instances in a single DeerFlow deployment by introducing list-based channel configuration and per-instance naming, alongside a Feishu WebSocket initialization rewrite to isolate event loops and avoid uvloop conflicts.
Changes:
- Extend
ChannelServiceconfig parsing to support list-format multi-instance channels with explicit unique instance names and a stored_base_type. - Update
ChannelManagerlogic to better handle multi-instance channel names when resolving streaming capability and inbound file readers. - Refactor Feishu channel startup to import/initialize
lark_oapiinside a dedicated thread with its own asyncio loop and a global init lock.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
backend/app/channels/feishu.py |
Reworks Feishu WS startup to isolate event loops and serialize SDK initialization across instances. |
backend/app/channels/service.py |
Adds list-format multi-instance channel config support and base-type resolution for channel startup/status. |
backend/app/channels/manager.py |
Adjusts streaming + inbound file reader resolution for multi-instance channel names. |
backend/app/channels/wecom.py |
Changes default working_message behavior for WeCom streaming replies. |
.gitignore |
Adds an ignore entry for a tracked source file (likely unintended). |
Comments suppressed due to low confidence (1)
backend/app/channels/feishu.py:98
- start() no longer checks for lark-oapi availability before marking the channel as running. If lark-oapi isn’t installed, the WS thread will error, but the channel will remain subscribed and appear running, and no friendly install hint is logged. Consider a non-import check (e.g., importlib.util.find_spec("lark_oapi")) in start() and/or setting _running back to False when _run_ws fails during initialization.
async def start(self) -> None:
if self._running:
return
app_id = self.config.get("app_id", "")
app_secret = self.config.get("app_secret", "")
domain = self.config.get("domain", "https://open.feishu.cn")
if not app_id or not app_secret:
logger.error("Feishu channel requires app_id and app_secret")
return
logger.info("[Feishu] using domain: %s", domain)
self._main_loop = asyncio.get_event_loop()
self._running = True
self.bus.subscribe_outbound(self._on_outbound)
# Both ws.Client construction and start() must happen in a dedicated
# thread with its own event loop. lark-oapi caches the running loop
# at construction time and later calls loop.run_until_complete(),
# which conflicts with an already-running uvloop.
self._thread = threading.Thread(
target=self._run_ws,
args=(app_id, app_secret, domain),
daemon=True,
)
self._thread.start()
logger.info("Feishu channel started")
Author
|
@copilot apply changes based on the comments in this thread |
…rcular import) - Fix status aggregation to use _base_type instead of name prefix - Add readiness signal in feishu.py with threading.Event - Update FeishuChannel to accept name parameter - Update service.py to pass name directly to channel constructor - SKIP manager.py changes to avoid circular import
d2918b2 to
71b5591
Compare
Remove wecom.py from .gitignore to track changes.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
backend/app/channels/feishu.py:100
start()marks the channel as running and subscribes to outbound messages before the WS thread has imported/initializedlark_oapiand built_api_client. If the SDK isn’t installed or initialization fails, the channel will still appear running, and outbound messages will be dropped (send()just logs a warning when_api_clientis None). Consider a fast availability check before starting the thread (e.g.importlib.util.find_spec), and/or waiting for_api_client_readywith a short timeout and rolling back_running+ outbound subscription when initialization fails (surfacing_api_client_init_errorin logs/status).
logger.info("[Feishu] using domain: %s", domain)
self._main_loop = asyncio.get_event_loop()
self._running = True
self.bus.subscribe_outbound(self._on_outbound)
# Both ws.Client construction and start() must happen in a dedicated
# thread with its own event loop. lark-oapi caches the running loop
# at construction time and later calls loop.run_until_complete(),
# which conflicts with an already-running uvloop.
self._thread = threading.Thread(
target=self._run_ws,
args=(app_id, app_secret, domain),
daemon=True,
)
self._thread.start()
logger.info("Feishu channel started")
minor fix shouldn't be included in this pr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull request overview
Adds support for running multiple Feishu (and other) channel instances in a single DeerFlow deployment by introducing list-based channel configuration and per-instance naming, alongside a Feishu WebSocket initialization rewrite to isolate event loops and avoid uvloop conflicts.
Changes:
ChannelServiceconfig parsing to support list-format multi-instance channels with explicit unique instance names and a stored_base_type.ChannelManagerlogic to better handle multi-instance channel names when resolving streaming capability and inbound file readers.lark_oapiinside a dedicated thread with its own asyncio loop and a global init lock.Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
backend/app/channels/feishu.pybackend/app/channels/service.pybackend/app/channels/manager.pybackend/app/channels/wecom.pyworking_messagebehavior for WeCom streaming replies..gitignoreComments suppressed due to low confidence (1)
backend/app/channels/feishu.py:98