Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
#
|
|
|
|
|
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
|
|
|
|
#
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
#
|
|
|
|
|
"""Chat channel runtime, embedded in the RAGFlow API server.
|
|
|
|
|
|
|
|
|
|
Continuously reconciles the running channel bots against the ``chat_channel``
|
|
|
|
|
table: newly added bots are started, deleted ones are stopped, and edited ones
|
|
|
|
|
(credential/type change) are restarted — without restarting the server. Inbound
|
|
|
|
|
messages are answered with a RAG completion routed through the conversation
|
|
|
|
|
wired to that bot. Replaces the standalone ``server.py`` entrypoint.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import hashlib
|
|
|
|
|
import importlib
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# Channel packages bundled under api/channels that self-register on import.
|
2026-06-23 17:45:31 +08:00
|
|
|
_BUNDLED_CHANNELS = (
|
|
|
|
|
"feishu",
|
|
|
|
|
"discord",
|
|
|
|
|
"telegram",
|
|
|
|
|
"line",
|
|
|
|
|
"wecom",
|
|
|
|
|
"qqbot",
|
|
|
|
|
"dingtalk",
|
|
|
|
|
"whatsapp",
|
|
|
|
|
)
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
|
|
|
|
|
# How often (seconds) to reconcile running channels against the database.
|
|
|
|
|
_RECONCILE_INTERVAL_SECS = 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _register_channels() -> None:
|
|
|
|
|
"""Import each bundled channel package so it self-registers a builder.
|
|
|
|
|
|
|
|
|
|
Each channel is imported independently: a missing optional dependency only
|
|
|
|
|
disables that one channel instead of taking down the whole channel server.
|
|
|
|
|
"""
|
|
|
|
|
for name in _BUNDLED_CHANNELS:
|
|
|
|
|
try:
|
|
|
|
|
importlib.import_module(f"api.channels.{name}")
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.warning("chat channel '%s' unavailable: %s", name, ex)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fingerprint(channel: str, credential: dict) -> str:
|
|
|
|
|
"""Stable hash of the parts that require a channel restart when changed."""
|
|
|
|
|
payload = json.dumps(
|
|
|
|
|
{"channel": channel, "credential": credential},
|
|
|
|
|
sort_keys=True,
|
|
|
|
|
default=str,
|
|
|
|
|
)
|
|
|
|
|
return hashlib.md5(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _desired_channels() -> dict:
|
|
|
|
|
"""Return {chat_channel.id: (channel_type, credential, fingerprint)} for enabled bots."""
|
|
|
|
|
from api.db.services.chat_channel_service import ChatChannelService
|
|
|
|
|
|
|
|
|
|
desired: dict = {}
|
|
|
|
|
for row in ChatChannelService.list_active():
|
|
|
|
|
credential = (row.config or {}).get("credential", {}) or {}
|
|
|
|
|
desired[row.id] = (row.channel, credential, _fingerprint(row.channel, credential))
|
|
|
|
|
return desired
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_one(account_id: str, channel: str, credential: dict):
|
|
|
|
|
"""Build a single Channel instance, or None if the type has no builder."""
|
|
|
|
|
from api.channels.core.registry import build_channels
|
|
|
|
|
|
|
|
|
|
# account_id == chat_channel.id.
|
|
|
|
|
instances = build_channels(
|
|
|
|
|
{"channels": {channel: {"accounts": {account_id: credential}}}}
|
|
|
|
|
)
|
|
|
|
|
return instances[0] if instances else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_chat_handler(ch):
|
|
|
|
|
"""Build the inbound-message handler bound to a single channel.
|
|
|
|
|
|
|
|
|
|
Mirrors the non-streaming path of ``session_completion``: the message is
|
|
|
|
|
appended to a per-end-user conversation under the dialog connected to the
|
|
|
|
|
bot, a RAG completion is run against that dialog, and the answer is sent
|
|
|
|
|
back. The connected dialog is resolved per message, so connection changes
|
|
|
|
|
take effect immediately without restarting the channel. Channels with no
|
|
|
|
|
connected dialog ignore inbound messages.
|
|
|
|
|
"""
|
|
|
|
|
from api.channels.core.base import IncomingMessage, OutgoingMessage
|
|
|
|
|
|
|
|
|
|
from api.db.services.chat_channel_service import ChatChannelService
|
|
|
|
|
from api.db.services.conversation_service import ConversationService, structure_answer
|
|
|
|
|
from api.db.services.dialog_service import DialogService, async_chat
|
|
|
|
|
from common.misc_utils import get_uuid
|
|
|
|
|
|
|
|
|
|
async def handle(msg: IncomingMessage) -> None:
|
|
|
|
|
if not (msg.text or "").strip():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# account_id == chat_channel.id; re-read so a re-connected dialog applies live.
|
|
|
|
|
e, cc = ChatChannelService.get_by_id(ch.account_id)
|
2026-06-17 15:52:13 +08:00
|
|
|
if not e or not cc.chat_id:
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
LOGGER.info(
|
|
|
|
|
"[%s:%s] no dialog connected; ignoring message",
|
|
|
|
|
ch.channel_id,
|
|
|
|
|
ch.account_id,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
2026-06-17 15:52:13 +08:00
|
|
|
e, dia = DialogService.get_by_id(cc.chat_id)
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
if not e:
|
2026-06-17 15:52:13 +08:00
|
|
|
LOGGER.warning("[%s:%s] connected dialog not found: %s", ch.channel_id, ch.account_id, cc.chat_id)
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
return
|
|
|
|
|
|
2026-06-17 15:52:13 +08:00
|
|
|
conv = ConversationService.get_or_create_for_channel(cc.chat_id, ch.account_id, msg.chat_id)
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
if conv is None:
|
|
|
|
|
LOGGER.warning("[%s:%s] failed to get conversation for chat %s", ch.channel_id, ch.account_id, msg.chat_id)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
message_id = get_uuid()
|
|
|
|
|
if not conv.message:
|
|
|
|
|
conv.message = []
|
|
|
|
|
conv.message.append({"role": "user", "content": msg.text, "id": message_id})
|
|
|
|
|
if not conv.reference:
|
|
|
|
|
conv.reference = []
|
|
|
|
|
conv.reference = [r for r in conv.reference if r]
|
|
|
|
|
conv.reference.append({"chunks": [], "doc_aggs": []})
|
|
|
|
|
|
|
|
|
|
history = []
|
|
|
|
|
for m in conv.message:
|
|
|
|
|
if m["role"] == "system":
|
|
|
|
|
continue
|
|
|
|
|
if m["role"] == "assistant" and not history:
|
|
|
|
|
continue
|
|
|
|
|
history.append(m)
|
|
|
|
|
|
|
|
|
|
answer_text = ""
|
|
|
|
|
try:
|
2026-06-23 17:45:31 +08:00
|
|
|
chat_kwargs = {"quote": False}
|
|
|
|
|
if "{knowledge}" in (dia.prompt_config or {}).get("system", ""):
|
|
|
|
|
chat_kwargs["knowledge"] = ""
|
|
|
|
|
async for ans in async_chat(dia, history, False, **chat_kwargs):
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
structure_answer(conv, ans, message_id, conv.id)
|
|
|
|
|
answer_text = (ans or {}).get("answer", "") or ""
|
|
|
|
|
ConversationService.update_by_id(conv.id, conv.to_dict())
|
|
|
|
|
break
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.exception("[%s:%s] completion failed: %s", ch.channel_id, ch.account_id, ex)
|
|
|
|
|
answer_text = f"**ERROR**: {ex}"
|
|
|
|
|
|
|
|
|
|
if answer_text:
|
|
|
|
|
await ch.send(
|
|
|
|
|
OutgoingMessage(
|
|
|
|
|
chat_id=msg.chat_id,
|
|
|
|
|
text=answer_text,
|
|
|
|
|
reply_to_message_id=msg.message_id or None,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return handle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _stop_channel(running: dict, account_id: str) -> None:
|
|
|
|
|
entry = running.pop(account_id, None)
|
|
|
|
|
if not entry:
|
|
|
|
|
return
|
|
|
|
|
ch = entry["ch"]
|
|
|
|
|
try:
|
|
|
|
|
await ch.stop()
|
|
|
|
|
LOGGER.info("stopped chat channel %s:%s", ch.channel_id, account_id)
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.error("failed to stop chat channel %s: %s", account_id, ex)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _start_channel(running: dict, account_id: str, channel: str, credential: dict, fp: str) -> bool:
|
|
|
|
|
"""Build, wire and start one channel. Returns True on success.
|
|
|
|
|
|
|
|
|
|
Any failure (e.g. invalid credentials) is contained here so a single bad bot
|
|
|
|
|
config never aborts the reconcile pass for the other channels.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
ch = _build_one(account_id, channel, credential)
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.error(
|
|
|
|
|
"failed to build chat channel %s (%s); check its credentials: %s",
|
|
|
|
|
account_id,
|
|
|
|
|
channel,
|
|
|
|
|
ex,
|
|
|
|
|
)
|
|
|
|
|
return False
|
|
|
|
|
if ch is None:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
ch.set_message_handler(_make_chat_handler(ch))
|
|
|
|
|
try:
|
|
|
|
|
await ch.start()
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.error("failed to start chat channel %s (%s): %s", account_id, channel, ex)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
running[account_id] = {"ch": ch, "fp": fp}
|
|
|
|
|
LOGGER.info("started chat channel %s:%s", ch.channel_id, account_id)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _reconcile(running: dict, failed: dict) -> None:
|
|
|
|
|
"""Diff desired (DB) vs running channels and apply start/stop/restart.
|
|
|
|
|
|
|
|
|
|
``failed`` remembers configs that could not be started so they are not
|
|
|
|
|
retried (and re-logged) every tick until their credentials change.
|
|
|
|
|
"""
|
|
|
|
|
desired = await asyncio.to_thread(_desired_channels)
|
|
|
|
|
|
|
|
|
|
# Stop channels that were removed or whose credentials/type changed.
|
|
|
|
|
for account_id in list(running.keys()):
|
|
|
|
|
changed = account_id in desired and desired[account_id][2] != running[account_id]["fp"]
|
|
|
|
|
if account_id not in desired or changed:
|
|
|
|
|
await _stop_channel(running, account_id)
|
|
|
|
|
|
|
|
|
|
# Drop remembered failures that are gone or whose config changed, so an
|
|
|
|
|
# edited (hopefully fixed) bot is retried.
|
|
|
|
|
for account_id in list(failed.keys()):
|
|
|
|
|
if account_id not in desired or desired[account_id][2] != failed[account_id]:
|
|
|
|
|
failed.pop(account_id, None)
|
|
|
|
|
|
2026-06-23 17:45:31 +08:00
|
|
|
active_whatsapp = any(channel == "whatsapp" for channel, _, _ in desired.values())
|
|
|
|
|
if not active_whatsapp:
|
|
|
|
|
active_whatsapp = any(
|
|
|
|
|
entry["ch"].channel_id == "whatsapp" for entry in running.values()
|
|
|
|
|
)
|
|
|
|
|
from api.channels.whatsapp.gateway import sync_whatsapp_gateway
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await sync_whatsapp_gateway(active_whatsapp)
|
|
|
|
|
except Exception:
|
|
|
|
|
LOGGER.exception("failed to sync WhatsApp gateway enabled=%s", active_whatsapp)
|
|
|
|
|
|
Feat: chat channels — connect assistants to external messaging bots (#15850)
### What problem does this PR solve?
#15844
Adds a **Chat channels** capability so a RAGFlow assistant (Dialog) can
be exposed as a bot on external messaging platforms (Feishu/Lark,
Discord, Telegram, Slack, WeCom, LINE, etc.). An admin configures a bot
in the UI, connects it to an assistant, and inbound messages are
answered from that assistant's knowledge base — replies are delivered
back on the channel.
**Feishu/Lark is implemented and tested end-to-end.** Discord, Telegram,
LINE, and WeCom are scaffolded against the same interface; the remaining
listed channels are tracked as follow-ups.
### Design
**Backend**
- New `chat_channel` table (`tenant_id`, `name`, `channel`, `config`
JSON holding `{credential: {...}}`, `dialog_id`, `status`) +
`ChatChannelService` and RESTful CRUD under `/api/v1/chat_channels`.
- Channel framework under `api/channels/`: a `core` registry +
per-channel packages that self-register a builder and implement a common
`Channel` interface (`start`/`stop`/`send` + inbound normalization) over
`IncomingMessage`/`OutgoingMessage`.
- Embedded **reconcile loop** in `ragflow_server`
(`api/channels/bootstrap.py`): loads enabled bots, and
starts/stops/restarts them as rows change (no server restart needed).
Inbound messages run the connected dialog via the non-streaming
completion path, keeping per-end-user conversation history.
- Missing optional channel SDKs degrade gracefully (channel skipped with
a warning; others unaffected). Channel-level errors are logged, not
crashed.
- Feishu's WebSocket client runs in a dedicated thread with its own
event loop to avoid cross-loop/contextvars conflicts with the channel
runtime.
**Frontend**
- **Settings → Chat channels** panel: available-channels grid +
configured-bots list with add/edit/delete and a **Connect assistant**
popup that binds a bot to a dialog.
- Brand icons via simple-icons / reused shared data-source assets, with
colored fallbacks for brands not available.
- Route, sidebar entry, i18n (en/zh), and a top-nav segment-boundary fix
so the settings page no longer highlights the Chat tab.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
### Notes
- DB: new `chat_channel` table is auto-created; `chat_channel.dialog_id`
is also covered by a `migrate_db` `alter_db_add_column` for existing
installs.
- Channel SDKs (`lark-oapi`, `discord.py`, `python-telegram-bot`,
`line-bot-sdk`, `wechatpy`, `aiohttp`) added to dependencies.
- Screenshots / per-channel credential docs to follow.
<img width="1338" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/042cb2f9-0dad-4e6a-bcf7-43ced4bbd704"
/>
<img width="1344" height="738" alt="Image"
src="https://github.com/user-attachments/assets/373cd08e-ec40-4c67-9c51-4d948b1ba617"
/>
<img width="672" height="887" alt="Image"
src="https://github.com/user-attachments/assets/5a34953f-a9a3-4c1e-869e-5eff0dc64c84"
/>
---------
2026-06-12 18:21:30 +08:00
|
|
|
# Start channels that are new (skip ones already known to fail with this config).
|
|
|
|
|
for account_id, (channel, credential, fp) in desired.items():
|
|
|
|
|
if account_id in running or failed.get(account_id) == fp:
|
|
|
|
|
continue
|
|
|
|
|
if not await _start_channel(running, account_id, channel, credential, fp):
|
|
|
|
|
failed[account_id] = fp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def run_channels(stop_event: threading.Event) -> None:
|
|
|
|
|
"""Reconcile and run channels until ``stop_event`` is set."""
|
|
|
|
|
_register_channels()
|
|
|
|
|
|
|
|
|
|
running: dict = {}
|
|
|
|
|
failed: dict = {}
|
|
|
|
|
try:
|
|
|
|
|
while not stop_event.is_set():
|
|
|
|
|
try:
|
|
|
|
|
await _reconcile(running, failed)
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.error("chat channel reconcile failed: %s", ex)
|
|
|
|
|
|
|
|
|
|
for _ in range(_RECONCILE_INTERVAL_SECS):
|
|
|
|
|
if stop_event.is_set():
|
|
|
|
|
break
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
finally:
|
|
|
|
|
LOGGER.info("Stopping chat channels...")
|
|
|
|
|
for account_id in list(running.keys()):
|
|
|
|
|
await _stop_channel(running, account_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_channel_server(stop_event: threading.Event) -> None:
|
|
|
|
|
"""Thread entrypoint: run the channel event loop, isolating any failure."""
|
|
|
|
|
try:
|
|
|
|
|
asyncio.run(run_channels(stop_event))
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOGGER.exception("Chat channel server crashed: %s", ex)
|