fix(wecom): 扫码绑定改用扫码授权登录端点,修复「请在企业微信客户端打开链接」+ bump 0.26.10

oauth_authorize_url 原用 open.weixin.qq.com/connect/oauth2/authorize(网页授权,
只能在企业微信客户端内打开),桌面浏览器 window.open 它 → 企业微信报「请在企业微信
客户端打开链接」,扫不了码。

改用扫码授权登录端点 login.work.weixin.qq.com/wwlogin/sso/login(login_type=CorpApp),
桌面浏览器渲染二维码,企业微信 App 扫码确认后回跳带 code,verify_state / get_user_id
逻辑不变。前置:redirect_uri 域名须配在应用「企业微信授权登录」可信域名(另一项设置)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-25 11:21:30 +08:00
parent 8ab1805df4
commit 5d3cd88e2c
4 changed files with 24 additions and 11 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-06-25(企业微信加「手填 userid」绑定:无 HTTPS 域名也能用企业微信推送 + bump 0.26.3) 最后更新:2026-06-25(修复企业微信扫码绑定报「请在企业微信客户端打开链接」:换扫码授权登录端点 + bump 0.26.10)
--- ---
@ -21,6 +21,12 @@
## 已完成关键能力 ## 已完成关键能力
### 2026-06-25 / 修复企业微信扫码绑定报「请在企业微信客户端打开链接」(bump 0.26.10)
- bug:`oauth_authorize_url()` 用的是 `open.weixin.qq.com/connect/oauth2/authorize`(网页授权),这条只能在企业微信客户端内置浏览器里打开;前端 `wecomBind()``window.open` 在**桌面浏览器**新标签打开它 → 企业微信返回「请在企业微信客户端打开链接」,扫不了码。注释里「桌面浏览器=出二维码扫」是误解(那是公众号行为,企微 oauth2/authorize 不出扫码页)。
- 修:换成**扫码授权登录**端点 `login.work.weixin.qq.com/wwlogin/sso/login?login_type=CorpApp&appid=CORPID&agentid=...&redirect_uri=...&state=...` —— 桌面浏览器会渲染二维码,用户用企业微信 App 扫码确认后回跳带 `code`,后续 `verify_state` / `get_user_id(code)` 换 userid 的逻辑完全不动。前置:redirect_uri 域名须在企业微信后台「应用 → 企业微信授权登录 → 可信域名」登记(与「网页授权可信域名」是两项不同设置)。
- 文件:`core/wechat/wecom.py`(`OAUTH_AUTHORIZE`→`WWLOGIN_SSO`、`oauth_authorize_url`)。
### 2026-06-25 / 修复 wechat_push 工具漏挂企业微信(只配企微也能推,bump 0.26.9) ### 2026-06-25 / 修复 wechat_push 工具漏挂企业微信(只配企微也能推,bump 0.26.9)
- bug:`wechat_push_available()` 只返回 `service.clawbot_enabled()`,完全没算企业微信。线上若只开了企业微信渠道(ClawBot 开关没开)→ 工具压根没注册到 agent → zcbot 照实回"我没有直接发企业微信的工具"(用户已绑企微仍推不出)。底层 `send_to_user` 其实早支持 `push_wecom`,门槛漏判而已。 - bug:`wechat_push_available()` 只返回 `service.clawbot_enabled()`,完全没算企业微信。线上若只开了企业微信渠道(ClawBot 开关没开)→ 工具压根没注册到 agent → zcbot 照实回"我没有直接发企业微信的工具"(用户已绑企微仍推不出)。底层 `send_to_user` 其实早支持 `push_wecom`,门槛漏判而已。

4
RUN.md
View File

@ -64,14 +64,14 @@
# WECOM_CORPID=ww... # 企业 ID(管理员:我的企业→企业信息) # WECOM_CORPID=ww... # 企业 ID(管理员:我的企业→企业信息)
# WECOM_AGENTID=1000002 # 自建应用 AgentId # WECOM_AGENTID=1000002 # 自建应用 AgentId
# WECOM_SECRET=... # 自建应用 Secret # WECOM_SECRET=... # 自建应用 Secret
# ZCBOT_PUBLIC_BASE_URL=https://zcbot.example.com # 可选,OAuth 回调主机(须在应用「网页授权可信域名」内;缺则取请求 base) # ZCBOT_PUBLIC_BASE_URL=https://zcbot.example.com # 可选,OAuth 回调主机(须在应用「企业微信授权登录」可信域名内;缺则取请求 base)
``` ```
> litellm 在 import 时副作用加载 .env;入口走 `main.py`,`.env` 自动生效。直跑 `python -c "from core.storage import ..."` 不经 litellm 链路时记得自己 `import litellm` 触发,或手动 `export ZCBOT_DB_URL=...` > litellm 在 import 时副作用加载 .env;入口走 `main.py`,`.env` 自动生效。直跑 `python -c "from core.storage import ..."` 不经 litellm 链路时记得自己 `import litellm` 触发,或手动 `export ZCBOT_DB_URL=...`
- **依赖**:`pip install -r requirements.txt`(已在 `.venv` 里;含 `bcrypt`、`segno`、`cryptography`)。 - **依赖**:`pip install -r requirements.txt`(已在 `.venv` 里;含 `bcrypt`、`segno`、`cryptography`)。
- **微信接入(ClawBot,§8.7)**:① `main.py db upgrade head` 带上 migration `0012`;② `.env``ZCBOT_WECHAT_BOT_ENABLED=1` + `ZCBOT_WECHAT_SECRET_KEY=<串>`;③ 用户登录后点**左栏 rail「微信」按钮**(`/static/wechat_bind.html` 仍保留作独立/嵌入入口)扫码绑定(需个人微信 8.0.70+ 且灰度到 ClawBot 插件)。绑定后在微信「微信 ClawBot」对话即走 zcbot;**主动推送需用户近 24h 在微信开口过一次**(冷启动/超期推不出,退邮件兜底)。 - **微信接入(ClawBot,§8.7)**:① `main.py db upgrade head` 带上 migration `0012`;② `.env``ZCBOT_WECHAT_BOT_ENABLED=1` + `ZCBOT_WECHAT_SECRET_KEY=<串>`;③ 用户登录后点**左栏 rail「微信」按钮**(`/static/wechat_bind.html` 仍保留作独立/嵌入入口)扫码绑定(需个人微信 8.0.70+ 且灰度到 ClawBot 插件)。绑定后在微信「微信 ClawBot」对话即走 zcbot;**主动推送需用户近 24h 在微信开口过一次**(冷启动/超期推不出,退邮件兜底)。
- **企业微信(渠道 B,纯推送,§8.7)**:① 管理员建自建应用 → 填 `WECOM_CORPID/AGENTID/SECRET`(+ 可见范围含目标用户);② `main.py db upgrade head`。**绑定两条路,任选**: - **企业微信(渠道 B,纯推送,§8.7)**:① 管理员建自建应用 → 填 `WECOM_CORPID/AGENTID/SECRET`(+ 可见范围含目标用户);② `main.py db upgrade head`。**绑定两条路,任选**:
- **手填 userid(无域名时,最省)**:rail「微信」modal 企业微信段填成员 userid(管理后台→通讯录→点成员→「账号」)→ 保存。**推送是出站调用,不需要域名/HTTPS**,这条最省事。 - **手填 userid(无域名时,最省)**:rail「微信」modal 企业微信段填成员 userid(管理后台→通讯录→点成员→「账号」)→ 保存。**推送是出站调用,不需要域名/HTTPS**,这条最省事。
- **扫码授权(OAuth,要 HTTPS 域名)**:管理员另配「网页授权可信域名」指向 zcbot 域名 + `ZCBOT_PUBLIC_BASE_URL`;用户点「扫码绑定」。回调 `/v1/wecom/oauth/callback` 公开(身份从 HMAC state 验)。 - **扫码授权登录(要 HTTPS 域名)**:管理员在应用→**「企业微信授权登录」**里把 zcbot 域名配进可信域名(注意不是「网页授权可信域名」,是另一项)+ 设 `ZCBOT_PUBLIC_BASE_URL`;用户点「扫码绑定」→ 桌面浏览器出二维码 → 企业微信 App 扫码确认。回调 `/v1/wecom/oauth/callback` 公开(身份从 HMAC state 验)。链接走 `login.work.weixin.qq.com/wwlogin/sso/login`(不是网页授权 `oauth2/authorize`,后者只能在企微客户端内打开 → 桌面浏览器会报「请在企业微信客户端打开链接」)。
- 绑定后简报/结果**无条件主动推**(不挑活跃度、无 24h 窗口),适合必达;不做对话(要对话用 ClawBot)。 - 绑定后简报/结果**无条件主动推**(不挑活跃度、无 24h 窗口),适合必达;不做对话(要对话用 ClawBot)。
- **PG**:`ZCBOT_DB_URL` 必填。本地 docker compose / 远端 dev / 生产任选;未设置时启动清晰报错,不引导 docker(§7.4)。 - **PG**:`ZCBOT_DB_URL` 必填。本地 docker compose / 远端 dev / 生产任选;未设置时启动清晰报错,不引导 docker(§7.4)。
- **Auth env**:`PLATFORM_KEY` + `JWT_SECRET` 任一缺失 web 启动 fail-fast。生成随机串:`python -c "import secrets; print(secrets.token_urlsafe(48))"`。 - **Auth env**:`PLATFORM_KEY` + `JWT_SECRET` 任一缺失 web 启动 fail-fast。生成随机串:`python -c "import secrets; print(secrets.token_urlsafe(48))"`。

View File

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

View File

@ -2,8 +2,8 @@
只做**出站推送**(不做入站对话): 只做**出站推送**(不做入站对话):
- `access_token`:`gettoken(corpid,secret)`,进程内缓存 ~2h线程安全errcode 失效即重取 - `access_token`:`gettoken(corpid,secret)`,进程内缓存 ~2h线程安全errcode 失效即重取
- OAuth 网页授权:`oauth_authorize_url()` 造扫码链接;`get_user_id(code)` 拿成员 userid - OAuth 扫码登录:`oauth_authorize_url()` 造扫码授权登录链接(桌面浏览器出二维码);
(绑定用,一次性)需管理员在应用配网页授权可信域名 `get_user_id(code)` 拿成员 userid(绑定用,一次性)需管理员在应用配企业微信授权登录可信域名
- 发送:`send_text / send_markdown / send_file`(file `media/upload` media_id,20MB) - 发送:`send_text / send_markdown / send_file`(file `media/upload` media_id,20MB)
- `state` HMAC 签名( user_id + TTL, CSRF):回调无 JWT,用户身份从 state - `state` HMAC 签名( user_id + TTL, CSRF):回调无 JWT,用户身份从 state
@ -24,7 +24,10 @@ from typing import Optional
import httpx import httpx
QYAPI = "https://qyapi.weixin.qq.com/cgi-bin" QYAPI = "https://qyapi.weixin.qq.com/cgi-bin"
OAUTH_AUTHORIZE = "https://open.weixin.qq.com/connect/oauth2/authorize" # 扫码授权登录(桌面浏览器渲染二维码,用企业微信 App 扫码)。
# 不能用 open.weixin.qq.com/connect/oauth2/authorize —— 那条是「网页授权」,只能在
# 企业微信客户端内打开,桌面浏览器会报「请在企业微信客户端打开链接」。
WWLOGIN_SSO = "https://login.work.weixin.qq.com/wwlogin/sso/login"
MAX_FILE_BYTES = 20 * 1024 * 1024 MAX_FILE_BYTES = 20 * 1024 * 1024
# access_token 进程内缓存 # access_token 进程内缓存
@ -137,13 +140,17 @@ def verify_state(state: str) -> Optional[str]:
def oauth_authorize_url(redirect_uri: str, state: str) -> str: def oauth_authorize_url(redirect_uri: str, state: str) -> str:
"""造网页授权链接。桌面浏览器打开 = 出二维码扫;企业微信内 = 静默授权。""" """造**扫码授权登录**链接:桌面浏览器打开会渲染二维码,用户用企业微信 App 扫码确认后
回跳到 redirect_uri code(后续 auth/getuserinfo userid 不变)
注意:redirect_uri 域名须在企业微信后台应用 企业微信授权登录 可信域名里登记,
网页授权可信域名是两项不同设置"""
from urllib.parse import quote from urllib.parse import quote
return ( return (
f"{OAUTH_AUTHORIZE}?appid={_corpid()}" f"{WWLOGIN_SSO}?login_type=CorpApp&appid={_corpid()}"
f"&agentid={_agentid()}"
f"&redirect_uri={quote(redirect_uri, safe='')}" f"&redirect_uri={quote(redirect_uri, safe='')}"
f"&response_type=code&scope=snsapi_base&agentid={_agentid()}" f"&state={quote(state, safe='')}"
f"&state={quote(state, safe='')}#wechat_redirect"
) )