lark-oapi 1.6.2 在内网代理下连不上飞书的根因排查
TL;DR
lark-oapi 从 1.5.5 升到 1.6.2 后,部署在内网(必须走 HTTP 代理出公网)的飞书机器人 WebSocket 长连接不通了。原因不在 websockets 换底层、也不在 HTTP_PROXY 环境变量,而是 SDK 源码里显式写了 proxy=None:
# lark_oapi/ws/client.py (1.6.2)
def _ws_connect_kwargs():
params = inspect.signature(websockets.connect).parameters
if "proxy" in params:
return {"proxy": None} # ← 主动关掉 websockets 15 的自动代理探测
return {}
修复方式两选一:
- 稳妥:钉
lark-oapi==1.5.5 - 临时升级:monkey-patch 把
_ws_connect_kwargs换成返回{},让websockets 15的默认行为接管(会自动读https_proxy)
下面是完整过程,给后面踩同样坑的人作参考。
背景
项目 vibedp 是一个 YAML 驱动的 AI Agent 平台,部署在携程内网。机器人走飞书 WebSocket 长连接(lark-oapi SDK),通过公司 HTTP 代理 proxy.corp.com:8080 出公网。
某天升级后上线失败,现象是 Lark 模式起不来,连不通 open.feishu.cn。降级回上个版本又能用。
排查过程
第 1 步:怀疑最近的重构
最近做过一次 refactor/restructure-architecture,拆了模块、挪了文件。直觉上像是重构把 Lark 链路搞坏了。
对比了 f1e668...HEAD 这段时间 vibedp/integrations/lark_integration.py 的 diff——几乎没动,只是 make format 把两行拼接的字符串合并成了一行。entry.py 里 Lark 启动序列也和重构前一致。
重构这条线排除。
第 2 步:检查依赖
uv.lock 里写的是 lark-oapi 1.5.3,但用户报告实际装的是 1.6.2。
发现部署链路是:
uv pip install vip
但 pyproject.toml 里 lark-oapi 没有版本约束:
dependencies = [
"lark-oapi", # ← 任何版本都行
...
]
第一个结论:依赖漂移。Pin 住版本:
"lark-oapi==1.5.5",
生效,问题消失。但不知道 1.6.2 到底改了什么导致代理失效。
第 3 步:对比 1.5.5 → 1.6.2 的 commit
GitHub compare API 拉下来,5 个 commit。前 4 个都是新加的 channel/ 高层能力层(FeishuChannel 那套),跟底层 WS 无关。
最后一个引人注目:
ae7e84e fix: support websockets 15
这就是嫌疑人。但还不知道具体怎么坏的。
第 4 步:第一个错误假设
先自己推理:websockets 15 换了 IO 栈,不再通过 aiohttp,所以 trust_env=True 读环境变量代理的能力没了。
翻 websockets 官方文档打脸:14.2 开始原生支持环境变量代理,查找顺序:
no_proxy→ 命中直连wss_proxy/ws_proxy(WebSocket 专用,优先级最高)- 系统 SOCKS 代理
https_proxy(wss://和ws://都走)http_proxy(只给ws://)
底层用 urllib.request.getproxies() 读。
第 5 步:验证环境
让用户在容器里跑:
$ python -c "import websockets; print(websockets.__version__)"
15.0.1
$ python -c "from urllib.request import getproxies; print(getproxies())"
{}
这里一度以为找到答案了——getproxies() 返回空,说明进程环境里根本没代理变量。但用户接着查了 supervisor 启动的 vibedp 进程的 /proc/<pid>/environ:
https_proxy=http://proxy.corp.com:8080
http_proxy=http://proxy.corp.com:8080
(之前的 python -c 是 docker exec 进去的 shell,跟 supervisor 启的进程不是同一个环境。)
也就是说 supervisor 启的 vibedp 进程里代理变量是对的,但 websockets 15 就是没走代理。
第 6 步:直接看源码
走到这一步应该一开始就做。拉 1.6.2 的 lark_oapi/ws/client.py:
def _ws_connect_kwargs():
params = inspect.signature(websockets.connect).parameters
if "proxy" in params:
# websockets 15 enables environment proxy discovery by default. The SDK
# historically connected directly, so preserve that behavior when the
# parameter exists.
return {"proxy": None}
return {}
然后在 _connect 里:
conn = await websockets.connect(conn_url, **_ws_connect_kwargs())
水落石出。
根因
lark-oapi 1.6.2 升级到兼容 websockets 15 的时候,维护者发现 websockets 15 默认会读系统代理配置。他们担心这改变 SDK 历史行为(之前的 websockets 13/14 不支持 proxy= 参数,相当于总是直连),于是主动传 proxy=None 关掉了自动探测。
注释原话:preserve that behavior when the parameter exists。
这个决策对”在公网直连飞书”的用户是正确的(没变化),但对必须走代理的内网部署用户是破坏性的——之前版本能通,是因为 websockets 13/14 的默认行为恰好能读到代理环境变量;升到 15 之后这条路被 proxy=None 堵死了。
用户环境里 https_proxy 配了也没用,因为 websockets.connect(url, proxy=None) 优先级高于环境变量。
为什么 1.5.5 能连
对照 1.5.5 的同一处代码:
# lark_oapi/ws/client.py (1.5.5)
conn = await websockets.connect(conn_url)
没有 proxy= 参数。当时装的 websockets 是 13 或 14 早期版本,connect() 签名里没有 proxy 参数,走默认行为。具体怎么读到代理的不重要——重点是没有显式禁用。
修复方案
方案 A:钉版本(推荐)
# pyproject.toml
dependencies = [
"lark-oapi==1.5.5",
...
]
方案 B:Monkey-patch(想用 1.6.2)
在 entry.py 顶部(或任何应用代码的入口):
import lark_oapi.ws.client as _ws_client
def _enable_proxy_discovery():
"""让 websockets 15 的环境变量代理探测重新生效。"""
return {}
_ws_client._ws_connect_kwargs = _enable_proxy_discovery
这样 websockets.connect(url) 不再收到 proxy=None,会按默认规则读 https_proxy 环境变量。
方案 C:给上游提 issue/PR
lark_oapi.ws.Client.__init__ 应该暴露 proxy 参数,或者至少在 https_proxy 存在时不强制 proxy=None。内网部署是很常见的企业场景,值得上游修。
教训
几个可以做得更好的地方:
- Pin 依赖版本。
"lark-oapi"这种无约束的写法在协作项目里是定时炸弹。PyPI 随便一个小版本上游发布就可能改变你的行为。 - 先看源码,再推理行为。这次排查我至少绕了两圈:先推理
websockets换了 IO 栈(错),再推理getproxies()读不到代理(部分对但不是主因)。如果第一步就去看lark_oapi.ws.client.py的 diff,十分钟就能看出proxy=None那行。 - 注释里”保持历史行为”的决策值得警惕。升级传递依赖时,”不破坏现有用户”听起来是好话,但对不同使用场景可能意义相反。这次
proxy=None对公网用户是零变更,对内网代理用户是完全不能连。
附:完整时间线
| 步骤 | 动作 | 结论 |
|---|---|---|
| 1 | 对比重构前后 diff | Lark 链路代码几乎没变,不是重构问题 |
| 2 | 查实际装的版本 | pyproject.toml 无约束 + CI 不走 lock → 装了 1.6.2 |
| 3 | 对比 1.5.5→1.6.2 的 commit | 嫌疑在 fix: support websockets 15 |
| 4 | 推理 websockets 15 不读代理 | 错,实际支持 |
| 5 | 验证环境变量 | supervisor 注入的变量是对的 |
| 6 | 看 SDK 源码 | 发现 proxy=None 硬编码 |
如果你也在用 lark-oapi + 内网代理,这个坑还在最新版本里(写这篇文章时)。升级前留意一下。