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:
parent
8ab1805df4
commit
5d3cd88e2c
|
|
@ -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
4
RUN.md
|
|
@ -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))"`。
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue