From f8bef42a047f1822e480c723999b954422b4b96f Mon Sep 17 00:00:00 2001 From: Ryker_Feng <90562015+18062706139fcz@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:53:30 +0800 Subject: [PATCH] fix(wechat): validate timing configuration (#4280) --- README.md | 5 ++++- backend/AGENTS.md | 1 + backend/app/channels/wechat.py | 6 ++++-- backend/tests/test_wechat_channel.py | 27 +++++++++++++++++++++++++++ config.example.yaml | 7 ++++--- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8c47ed937..adfd91f0f 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,10 @@ channels: 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 + polling_timeout: 35 # timing values must be positive finite seconds + polling_retry_delay: 5 + qrcode_poll_interval: 2 + qrcode_poll_timeout: 180 state_dir: ./.deer-flow/wechat/state max_inbound_image_bytes: 20971520 max_outbound_image_bytes: 20971520 diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 079c5ac63..bd58d8f50 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -546,6 +546,7 @@ The cached value is reused for both the blocking (`runs.wait`) and streaming (`_ - Disabled by default. It is a user-binding layer on top of the existing `channels.*` runtime config, not a replacement for provider bot credentials. - No public IP, OAuth callback URL, or provider webhook route is required by the current implementation. - Telegram uses a deep-link `/start ` flow over the existing long-polling worker. Slack, Discord, Feishu/Lark, DingTalk, WeChat, and WeCom use `/connect ` over their existing outbound channel workers. +- WeChat timing settings (`polling_timeout`, `polling_retry_delay`, `qrcode_poll_interval`, `qrcode_poll_timeout`) accept only positive finite seconds; invalid values fall back to their defaults so polling cannot enter a hot loop or sleep forever. - Frontend APIs: `GET /api/channels/providers`, `GET /api/channels/connections`, `POST /api/channels/{provider}/connect`, and `DELETE /api/channels/connections/{connection_id}`. - Browser APIs remain protected by normal Gateway auth/CSRF. Provider messages arrive through the already-configured channel workers. - Provider-level `connection_status` reflects the user's newest connection row. With no binding it is `not_connected`, except in auth-disabled local mode where a configured running channel reports `connected` because all channel messages already route to the default user. diff --git a/backend/app/channels/wechat.py b/backend/app/channels/wechat.py index c48fcc8f6..194f7e78e 100644 --- a/backend/app/channels/wechat.py +++ b/backend/app/channels/wechat.py @@ -8,6 +8,7 @@ import binascii import hashlib import json import logging +import math import mimetypes import secrets import tempfile @@ -1456,9 +1457,10 @@ class WechatChannel(Channel): @staticmethod def _coerce_float(value: Any, default: float) -> float: try: - return float(value) - except (TypeError, ValueError): + parsed = float(value) + except (OverflowError, TypeError, ValueError): return default + return parsed if math.isfinite(parsed) and parsed > 0 else default @staticmethod def _coerce_int(value: Any, default: int) -> int: diff --git a/backend/tests/test_wechat_channel.py b/backend/tests/test_wechat_channel.py index 0ae64e26b..07c926509 100644 --- a/backend/tests/test_wechat_channel.py +++ b/backend/tests/test_wechat_channel.py @@ -84,6 +84,33 @@ class _MockAsyncClient: return None +def test_timing_config_requires_positive_finite_values(): + from app.channels.wechat import WechatChannel + + timing_defaults = { + "polling_timeout": WechatChannel.DEFAULT_POLLING_TIMEOUT, + "polling_retry_delay": WechatChannel.DEFAULT_RETRY_DELAY, + "qrcode_poll_interval": WechatChannel.DEFAULT_QRCODE_POLL_INTERVAL, + "qrcode_poll_timeout": WechatChannel.DEFAULT_QRCODE_POLL_TIMEOUT, + } + attributes = { + "polling_timeout": "_polling_timeout", + "polling_retry_delay": "_retry_delay", + "qrcode_poll_interval": "_qrcode_poll_interval", + "qrcode_poll_timeout": "_qrcode_poll_timeout", + } + + for invalid in (0, -1, float("nan"), float("inf"), float("-inf"), 10**1000): + channel = WechatChannel( + bus=MessageBus(), + config={"bot_token": "test-token", **dict.fromkeys(timing_defaults, invalid)}, + ) + assert {key: getattr(channel, attributes[key]) for key in timing_defaults} == timing_defaults + + channel = WechatChannel(bus=MessageBus(), config={"bot_token": "test-token", "polling_retry_delay": "0.25"}) + assert channel._retry_delay == 0.25 + + def test_handle_update_publishes_private_chat_message(): from app.channels.wechat import WechatChannel diff --git a/config.example.yaml b/config.example.yaml index 14b5ab997..53759c105 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1856,11 +1856,12 @@ run_ownership: # # Optional: sent as SKRouteTag header when provided # route_tag: "" # allowed_users: [] # empty = allow all -# # Optional: long-polling timeout in seconds +# # Optional: timing values must be positive finite seconds # polling_timeout: 35 -# # Optional: QR poll interval in seconds when qrcode_login_enabled is true +# polling_retry_delay: 5 +# # QR poll interval when qrcode_login_enabled is true # qrcode_poll_interval: 2 -# # Optional: QR bootstrap timeout in seconds +# # QR bootstrap timeout # qrcode_poll_timeout: 180 # # Optional: persist getupdates cursor under the gateway container volume # state_dir: ./.deer-flow/wechat/state