diff --git a/PROGRESS.md b/PROGRESS.md index cacaa82..becbf75 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `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) - bug:`wechat_push_available()` 只返回 `service.clawbot_enabled()`,完全没算企业微信。线上若只开了企业微信渠道(ClawBot 开关没开)→ 工具压根没注册到 agent → zcbot 照实回"我没有直接发企业微信的工具"(用户已绑企微仍推不出)。底层 `send_to_user` 其实早支持 `push_wecom`,门槛漏判而已。 diff --git a/RUN.md b/RUN.md index e196920..d9e72b4 100644 --- a/RUN.md +++ b/RUN.md @@ -64,14 +64,14 @@ # WECOM_CORPID=ww... # 企业 ID(管理员:我的企业→企业信息) # WECOM_AGENTID=1000002 # 自建应用 AgentId # 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=...`。 - **依赖**:`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 在微信开口过一次**(冷启动/超期推不出,退邮件兜底)。 - **企业微信(渠道 B,纯推送,§8.7)**:① 管理员建自建应用 → 填 `WECOM_CORPID/AGENTID/SECRET`(+ 可见范围含目标用户);② `main.py db upgrade head`。**绑定两条路,任选**: - **手填 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)。 - **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))"`。 diff --git a/core/__init__.py b/core/__init__.py index 3529e1a..31edd69 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.26.9" +__version__ = "0.26.10" diff --git a/core/wechat/wecom.py b/core/wechat/wecom.py index de298b6..b666f36 100644 --- a/core/wechat/wecom.py +++ b/core/wechat/wecom.py @@ -2,8 +2,8 @@ 只做**出站推送**(不做入站对话): - `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)。 - `state` HMAC 签名(绑 user_id + 短 TTL,防 CSRF):回调无 JWT,用户身份从 state 来。 @@ -24,7 +24,10 @@ from typing import Optional import httpx 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 # access_token 进程内缓存 @@ -137,13 +140,17 @@ def verify_state(state: str) -> Optional[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 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"&response_type=code&scope=snsapi_base&agentid={_agentid()}" - f"&state={quote(state, safe='')}#wechat_redirect" + f"&state={quote(state, safe='')}" )