feat: add WeChat channel integration (#1869)
* feat: add WeChat channel integration * fix(backend): recover stale channel threads and align upload artifact handling * refactor(wechat): reduce scope and restore QR bootstrap * fix(backend): sort manager imports for Ruff lint * fix(tests): add missing patch import in test_channels.py * Update backend/app/channels/wechat.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update backend/app/channels/manager.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(wechat): streamline allowed file extensions initialization and clean up test file --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
90299e2710
commit
fa96acdf4b
26
README.md
26
README.md
|
|
@ -368,6 +368,7 @@ DeerFlow supports receiving tasks from messaging apps. Channels auto-start when
|
||||||
| Telegram | Bot API (long-polling) | Easy |
|
| Telegram | Bot API (long-polling) | Easy |
|
||||||
| Slack | Socket Mode | Moderate |
|
| Slack | Socket Mode | Moderate |
|
||||||
| Feishu / Lark | WebSocket | Moderate |
|
| Feishu / Lark | WebSocket | Moderate |
|
||||||
|
| WeChat | Tencent iLink (long-polling) | Moderate |
|
||||||
| WeCom | WebSocket | Moderate |
|
| WeCom | WebSocket | Moderate |
|
||||||
|
|
||||||
**Configuration in `config.yaml`:**
|
**Configuration in `config.yaml`:**
|
||||||
|
|
@ -412,6 +413,19 @@ channels:
|
||||||
bot_token: $TELEGRAM_BOT_TOKEN
|
bot_token: $TELEGRAM_BOT_TOKEN
|
||||||
allowed_users: [] # empty = allow all
|
allowed_users: [] # empty = allow all
|
||||||
|
|
||||||
|
wechat:
|
||||||
|
enabled: false
|
||||||
|
bot_token: $WECHAT_BOT_TOKEN
|
||||||
|
ilink_bot_id: $WECHAT_ILINK_BOT_ID
|
||||||
|
qrcode_login_enabled: true # optional: allow first-time QR bootstrap when bot_token is absent
|
||||||
|
allowed_users: [] # empty = allow all
|
||||||
|
polling_timeout: 35
|
||||||
|
state_dir: ./.deer-flow/wechat/state
|
||||||
|
max_inbound_image_bytes: 20971520
|
||||||
|
max_outbound_image_bytes: 20971520
|
||||||
|
max_inbound_file_bytes: 52428800
|
||||||
|
max_outbound_file_bytes: 52428800
|
||||||
|
|
||||||
# Optional: per-channel / per-user session settings
|
# Optional: per-channel / per-user session settings
|
||||||
session:
|
session:
|
||||||
assistant_id: mobile-agent # custom agent names are also supported here
|
assistant_id: mobile-agent # custom agent names are also supported here
|
||||||
|
|
@ -445,6 +459,10 @@ SLACK_APP_TOKEN=xapp-...
|
||||||
FEISHU_APP_ID=cli_xxxx
|
FEISHU_APP_ID=cli_xxxx
|
||||||
FEISHU_APP_SECRET=your_app_secret
|
FEISHU_APP_SECRET=your_app_secret
|
||||||
|
|
||||||
|
# WeChat iLink
|
||||||
|
WECHAT_BOT_TOKEN=your_ilink_bot_token
|
||||||
|
WECHAT_ILINK_BOT_ID=your_ilink_bot_id
|
||||||
|
|
||||||
# WeCom
|
# WeCom
|
||||||
WECOM_BOT_ID=your_bot_id
|
WECOM_BOT_ID=your_bot_id
|
||||||
WECOM_BOT_SECRET=your_bot_secret
|
WECOM_BOT_SECRET=your_bot_secret
|
||||||
|
|
@ -470,6 +488,14 @@ WECOM_BOT_SECRET=your_bot_secret
|
||||||
3. Under **Events**, subscribe to `im.message.receive_v1` and select **Long Connection** mode.
|
3. Under **Events**, subscribe to `im.message.receive_v1` and select **Long Connection** mode.
|
||||||
4. Copy the App ID and App Secret. Set `FEISHU_APP_ID` and `FEISHU_APP_SECRET` in `.env` and enable the channel in `config.yaml`.
|
4. Copy the App ID and App Secret. Set `FEISHU_APP_ID` and `FEISHU_APP_SECRET` in `.env` and enable the channel in `config.yaml`.
|
||||||
|
|
||||||
|
**WeChat Setup**
|
||||||
|
|
||||||
|
1. Enable the `wechat` channel in `config.yaml`.
|
||||||
|
2. Either set `WECHAT_BOT_TOKEN` in `.env`, or set `qrcode_login_enabled: true` for first-time QR bootstrap.
|
||||||
|
3. When `bot_token` is absent and QR bootstrap is enabled, watch backend logs for the QR content returned by iLink and complete the binding flow.
|
||||||
|
4. After the QR flow succeeds, DeerFlow persists the acquired token under `state_dir` for later restarts.
|
||||||
|
5. For Docker Compose deployments, keep `state_dir` on a persistent volume so the `get_updates_buf` cursor and saved auth state survive restarts.
|
||||||
|
|
||||||
**WeCom Setup**
|
**WeCom Setup**
|
||||||
|
|
||||||
1. Create a bot on the WeCom AI Bot platform and obtain the `bot_id` and `bot_secret`.
|
1. Create a bot on the WeCom AI Bot platform and obtain the `bot_id` and `bot_secret`.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import mimetypes
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from collections.abc import Awaitable, Callable, Mapping
|
from collections.abc import Awaitable, Callable, Mapping
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -37,6 +38,7 @@ CHANNEL_CAPABILITIES = {
|
||||||
"feishu": {"supports_streaming": True},
|
"feishu": {"supports_streaming": True},
|
||||||
"slack": {"supports_streaming": False},
|
"slack": {"supports_streaming": False},
|
||||||
"telegram": {"supports_streaming": False},
|
"telegram": {"supports_streaming": False},
|
||||||
|
"wechat": {"supports_streaming": False},
|
||||||
"wecom": {"supports_streaming": True},
|
"wecom": {"supports_streaming": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +80,24 @@ async def _read_wecom_inbound_file(file_info: dict[str, Any], client: httpx.Asyn
|
||||||
return decrypt_file(data, aeskey)
|
return decrypt_file(data, aeskey)
|
||||||
|
|
||||||
|
|
||||||
|
async def _read_wechat_inbound_file(file_info: dict[str, Any], client: httpx.AsyncClient) -> bytes | None:
|
||||||
|
raw_path = file_info.get("path")
|
||||||
|
if isinstance(raw_path, str) and raw_path.strip():
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(Path(raw_path).read_bytes)
|
||||||
|
except OSError:
|
||||||
|
logger.exception("[Manager] failed to read WeChat inbound file from local path: %s", raw_path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
full_url = file_info.get("full_url")
|
||||||
|
if isinstance(full_url, str) and full_url.strip():
|
||||||
|
return await _read_http_inbound_file({"url": full_url}, client)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
register_inbound_file_reader("wecom", _read_wecom_inbound_file)
|
register_inbound_file_reader("wecom", _read_wecom_inbound_file)
|
||||||
|
register_inbound_file_reader("wechat", _read_wechat_inbound_file)
|
||||||
|
|
||||||
|
|
||||||
class InvalidChannelSessionConfigError(ValueError):
|
class InvalidChannelSessionConfigError(ValueError):
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ _CHANNEL_REGISTRY: dict[str, str] = {
|
||||||
"feishu": "app.channels.feishu:FeishuChannel",
|
"feishu": "app.channels.feishu:FeishuChannel",
|
||||||
"slack": "app.channels.slack:SlackChannel",
|
"slack": "app.channels.slack:SlackChannel",
|
||||||
"telegram": "app.channels.telegram:TelegramChannel",
|
"telegram": "app.channels.telegram:TelegramChannel",
|
||||||
|
"wechat": "app.channels.wechat:WechatChannel",
|
||||||
"wecom": "app.channels.wecom:WeComChannel",
|
"wecom": "app.channels.wecom:WeComChannel",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -794,6 +794,36 @@ checkpointer:
|
||||||
# bot_token: $TELEGRAM_BOT_TOKEN
|
# bot_token: $TELEGRAM_BOT_TOKEN
|
||||||
# allowed_users: [] # empty = allow all
|
# allowed_users: [] # empty = allow all
|
||||||
#
|
#
|
||||||
|
# wechat:
|
||||||
|
# enabled: false
|
||||||
|
# bot_token: $WECHAT_BOT_TOKEN
|
||||||
|
# ilink_bot_id: $WECHAT_ILINK_BOT_ID
|
||||||
|
# # Optional: allow first-time QR bootstrap when bot_token is absent
|
||||||
|
# qrcode_login_enabled: true
|
||||||
|
# # Optional: sent as iLink-App-Id header when provided
|
||||||
|
# ilink_app_id: ""
|
||||||
|
# # Optional: sent as SKRouteTag header when provided
|
||||||
|
# route_tag: ""
|
||||||
|
# allowed_users: [] # empty = allow all
|
||||||
|
# # Optional: long-polling timeout in seconds
|
||||||
|
# polling_timeout: 35
|
||||||
|
# # Optional: QR poll interval in seconds when qrcode_login_enabled is true
|
||||||
|
# qrcode_poll_interval: 2
|
||||||
|
# # Optional: QR bootstrap timeout in seconds
|
||||||
|
# qrcode_poll_timeout: 180
|
||||||
|
# # Optional: persist getupdates cursor under the gateway container volume
|
||||||
|
# state_dir: ./.deer-flow/wechat/state
|
||||||
|
# # Optional: max inbound image size in bytes before skipping download
|
||||||
|
# max_inbound_image_bytes: 20971520
|
||||||
|
# # Optional: max outbound image size in bytes before skipping upload
|
||||||
|
# max_outbound_image_bytes: 20971520
|
||||||
|
# # Optional: max inbound file size in bytes before skipping download
|
||||||
|
# max_inbound_file_bytes: 52428800
|
||||||
|
# # Optional: max outbound file size in bytes before skipping upload
|
||||||
|
# max_outbound_file_bytes: 52428800
|
||||||
|
# # Optional: allowed file extensions for regular file receive/send
|
||||||
|
# allowed_file_extensions: [".txt", ".md", ".pdf", ".csv", ".json", ".yaml", ".yml", ".xml", ".html", ".log", ".zip", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".rtf"]
|
||||||
|
#
|
||||||
# # Optional: channel-level session overrides
|
# # Optional: channel-level session overrides
|
||||||
# session:
|
# session:
|
||||||
# assistant_id: mobile-agent # custom agent names are supported here too
|
# assistant_id: mobile-agent # custom agent names are supported here too
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue