From 8ab1805df4aac5db660c394911dc55aeee813b97 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 25 Jun 2026 11:09:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(wechat):=20wechat=5Fpush=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=BC=8F=E6=8C=82=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=20+=20=E6=8F=90=E5=8F=96=20active=5Fchannels=20=E5=8D=95?= =?UTF-8?q?=E4=B8=80=E7=9C=9F=E7=9B=B8=E6=BA=90=20+=20bump=200.26.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:wechat_push_available() 只看 clawbot_enabled(),没算企业微信。线上若只开 企微渠道(ClawBot 开关没开)→ 工具压根不注册到 agent → zcbot 照实回"没有直接 发企业微信的工具",用户已绑企微仍推不出。底层 send_to_user 早支持 push_wecom, 纯属注册门槛漏判。 修:提取 service.active_channels() 作渠道清单唯一真相源,门槛(wechat_push_available) 与投递(send_to_user)都引它,加渠道只改一处,根除"两处各列各的"这类偏差。 工具描述把 ~24h 窗口注明为 ClawBot-only(企业微信无窗口约束)。 纯内部重构,对外契约不变;test_secret_host_tools 8/8 过。 Co-Authored-By: Claude Opus 4.8 (1M context) --- PROGRESS.md | 6 ++++++ core/__init__.py | 2 +- core/wechat/service.py | 25 +++++++++++++++++++------ tools/wechat_bot.py | 14 ++++++++------ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f418515..cacaa82 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,6 +21,12 @@ ## 已完成关键能力 +### 2026-06-25 / 修复 wechat_push 工具漏挂企业微信(只配企微也能推,bump 0.26.9) + +- bug:`wechat_push_available()` 只返回 `service.clawbot_enabled()`,完全没算企业微信。线上若只开了企业微信渠道(ClawBot 开关没开)→ 工具压根没注册到 agent → zcbot 照实回"我没有直接发企业微信的工具"(用户已绑企微仍推不出)。底层 `send_to_user` 其实早支持 `push_wecom`,门槛漏判而已。 +- 修:提取 `service.active_channels()` 作渠道清单**唯一真相源** —— `wechat_push_available()` 改成 `bool(active_channels())`、`send_to_user()` 改成 `for ch in active_channels(): _DISPATCH[ch](...)`,门槛与投递同源,加渠道只改一处,根除"两处各列各的"这类漏判。工具描述把「~24h 窗口」注明为 ClawBot-only(企业微信无窗口约束),避免 agent 在企微场景误判窗口限制。纯内部重构,对外契约不变;`test_secret_host_tools` 8/8 过。 +- 文件:`tools/wechat_bot.py`、`core/wechat/service.py`。 + ### 2026-06-25 / 企业微信加「手填 userid」绑定(无域名也能推,bump 0.26.3) - 痛点:企业微信只有 OAuth 扫码绑定那一路,而 OAuth 回调要落在 HTTPS 可信域名;用户暂无域名 → 卡住。关键认知:**企业微信推送是出站调用(gettoken/message_send 直连 qyapi),根本不需要域名**——只有"扫码拿 userid"那步要域名。 diff --git a/core/__init__.py b/core/__init__.py index 3cb2bf5..3529e1a 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.26.8" +__version__ = "0.26.9" diff --git a/core/wechat/service.py b/core/wechat/service.py index 7501230..13fe5bc 100644 --- a/core/wechat/service.py +++ b/core/wechat/service.py @@ -250,14 +250,27 @@ class DeliveryReport: return any(r.ok for r in self.results) +def active_channels() -> list[str]: + """部署级「哪些渠道开了」的**唯一真相源**:门槛判断(`wechat_push_available`) + 与投递(`send_to_user`)都引它,避免两处各列各的(曾漏判企业微信致工具不挂)。 + 加渠道只改这一处,门槛与投递自动一致。顺序即投递优先序。""" + from core.wechat.wecom import wecom_configured + chans: list[str] = [] + if clawbot_enabled(): + chans.append(_CLAWBOT) + if wecom_configured(): + chans.append(_WECOM) + return chans + + +_DISPATCH = {_CLAWBOT: push_clawbot, _WECOM: push_wecom} + + def send_to_user( user_id: UUID, text: str = "", file_path: Optional[str] = None ) -> DeliveryReport: - """渠道抽象:按用户已绑渠道投递。当前仅 ClawBot;企业微信(渠道 B)后续追加。""" + """渠道抽象:按 `active_channels()` 列出的已开渠道依次投递。""" report = DeliveryReport() - if clawbot_enabled(): - report.results.append(push_clawbot(user_id, text, file_path)) - from core.wechat.wecom import wecom_configured - if wecom_configured(): - report.results.append(push_wecom(user_id, text, file_path)) + for ch in active_channels(): + report.results.append(_DISPATCH[ch](user_id, text, file_path)) return report diff --git a/tools/wechat_bot.py b/tools/wechat_bot.py index f34462b..9f5a612 100644 --- a/tools/wechat_bot.py +++ b/tools/wechat_bot.py @@ -18,8 +18,9 @@ from .base import FileOutOfBounds, Tool def wechat_push_available() -> bool: - """任一微信渠道可用(当前 = ClawBot 开关在;后续 or 企业微信配齐)。""" - return service.clawbot_enabled() + """任一微信渠道开着就挂工具(ClawBot 个人微信 / 企业微信)。渠道清单的唯一真相源 + 是 `service.active_channels()`,与 `send_to_user` 的投递口径同源,不再各列各的。""" + return bool(service.active_channels()) _REASON_HINT = { @@ -33,10 +34,11 @@ class WechatPushTool(Tool): name = "wechat_push" description = ( "Proactively push a short text message (and optionally one result file, e.g. a .docx/.pdf " - "report) to the user's bound WeChat. Use when the user asks to send something to their " - "WeChat, or when a scheduled task should deliver its output there. NOTE: WeChat push only " - "works if the user has messaged the bot within the last ~24h; if it returns a window/binding " - "error, fall back to send_email. The file path is relative to the working directory." + "report) to the user's bound WeChat (personal WeChat via ClawBot, or WeCom/企业微信). Use " + "when the user asks to send something to their WeChat, or when a scheduled task should " + "deliver its output there. NOTE: the ~24h-window constraint applies ONLY to the personal " + "WeChat (ClawBot) channel — WeCom (企业微信) has no window limit. If it returns a " + "window/binding error, fall back to send_email. The file path is relative to the working directory." ) parameters = { "type": "object",