fix(wechat): wechat_push 工具漏挂企业微信 + 提取 active_channels 单一真相源 + bump 0.26.9

根因: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) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-25 11:09:21 +08:00
parent 23c5ab20e0
commit 8ab1805df4
4 changed files with 34 additions and 13 deletions

View File

@ -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) ### 2026-06-25 / 企业微信加「手填 userid」绑定(无域名也能推,bump 0.26.3)
- 痛点:企业微信只有 OAuth 扫码绑定那一路,而 OAuth 回调要落在 HTTPS 可信域名;用户暂无域名 → 卡住。关键认知:**企业微信推送是出站调用(gettoken/message_send 直连 qyapi),根本不需要域名**——只有"扫码拿 userid"那步要域名。 - 痛点:企业微信只有 OAuth 扫码绑定那一路,而 OAuth 回调要落在 HTTPS 可信域名;用户暂无域名 → 卡住。关键认知:**企业微信推送是出站调用(gettoken/message_send 直连 qyapi),根本不需要域名**——只有"扫码拿 userid"那步要域名。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.26.8" __version__ = "0.26.9"

View File

@ -250,14 +250,27 @@ class DeliveryReport:
return any(r.ok for r in self.results) 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( def send_to_user(
user_id: UUID, text: str = "", file_path: Optional[str] = None user_id: UUID, text: str = "", file_path: Optional[str] = None
) -> DeliveryReport: ) -> DeliveryReport:
"""渠道抽象:按用户已绑渠道投递。当前仅 ClawBot;企业微信(渠道 B)后续追加。""" """渠道抽象:按 `active_channels()` 列出的已开渠道依次投递"""
report = DeliveryReport() report = DeliveryReport()
if clawbot_enabled(): for ch in active_channels():
report.results.append(push_clawbot(user_id, text, file_path)) report.results.append(_DISPATCH[ch](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))
return report return report

View File

@ -18,8 +18,9 @@ from .base import FileOutOfBounds, Tool
def wechat_push_available() -> bool: def wechat_push_available() -> bool:
"""任一微信渠道可用(当前 = ClawBot 开关在;后续 or 企业微信配齐)。""" """任一微信渠道开着就挂工具(ClawBot 个人微信 / 企业微信)。渠道清单的唯一真相源
return service.clawbot_enabled() `service.active_channels()`, `send_to_user` 的投递口径同源,不再各列各的"""
return bool(service.active_channels())
_REASON_HINT = { _REASON_HINT = {
@ -33,10 +34,11 @@ class WechatPushTool(Tool):
name = "wechat_push" name = "wechat_push"
description = ( description = (
"Proactively push a short text message (and optionally one result file, e.g. a .docx/.pdf " "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 " "report) to the user's bound WeChat (personal WeChat via ClawBot, or WeCom/企业微信). Use "
"WeChat, or when a scheduled task should deliver its output there. NOTE: WeChat push only " "when the user asks to send something to their WeChat, or when a scheduled task should "
"works if the user has messaged the bot within the last ~24h; if it returns a window/binding " "deliver its output there. NOTE: the ~24h-window constraint applies ONLY to the personal "
"error, fall back to send_email. The file path is relative to the working directory." "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 = { parameters = {
"type": "object", "type": "object",