Skip to content

Feature/multiple feishu bot support#2404

Open
MarkHoch wants to merge 9 commits intobytedance:mainfrom
MarkHoch:feature/multiple-feishu-bot-support
Open

Feature/multiple feishu bot support#2404
MarkHoch wants to merge 9 commits intobytedance:mainfrom
MarkHoch:feature/multiple-feishu-bot-support

Conversation

@MarkHoch
Copy link
Copy Markdown

@MarkHoch MarkHoch commented Apr 21, 2026

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 ChannelService config parsing to support list-format multi-instance channels with explicit unique instance names and a stored _base_type.
  • Update ChannelManager logic to better handle multi-instance channel names when resolving streaming capability and inbound file readers.
  • Refactor Feishu channel startup to import/initialize lark_oapi inside 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")

…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
Copilot AI review requested due to automatic review settings April 21, 2026 12:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ChannelService config parsing to support list-format multi-instance channels with explicit unique instance names and a stored _base_type.
  • Update ChannelManager logic to better handle multi-instance channel names when resolving streaming capability and inbound file readers.
  • Refactor Feishu channel startup to import/initialize lark_oapi inside 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")

Comment thread backend/app/channels/service.py
Comment thread backend/app/channels/service.py Outdated
Comment thread backend/app/channels/manager.py
Comment thread backend/app/channels/manager.py
Comment thread backend/app/channels/feishu.py Outdated
@MarkHoch
Copy link
Copy Markdown
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
@MarkHoch MarkHoch force-pushed the feature/multiple-feishu-bot-support branch from d2918b2 to 71b5591 Compare April 21, 2026 22:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/initialized lark_oapi and 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_client is None). Consider a fast availability check before starting the thread (e.g. importlib.util.find_spec), and/or waiting for _api_client_ready with a short timeout and rolling back _running + outbound subscription when initialization fails (surfacing _api_client_init_error in 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")

Comment thread backend/app/channels/wecom.py Outdated
Comment thread backend/app/channels/service.py
Comment thread backend/app/channels/service.py
minor fix shouldn't be included in this pr
Copy link
Copy Markdown
Author

@MarkHoch MarkHoch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multiple feishu bot support

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants