飞书机器人回调本地调不通?用 cpolar 打通 HTTPS 事件订阅链路

发布时间:2026/7/10 22:25:28
飞书机器人回调本地调不通?用 cpolar 打通 HTTPS 事件订阅链路 飞书机器人回调本地调不通用 cpolar 打通 HTTPS 事件订阅链路调飞书机器人或飞书应用事件订阅时很多人第一步就卡在“请求地址校验失败”。本地服务明明已经启动curl http://127.0.0.1:3000/feishu/events也能返回结果但把地址填进飞书开放平台后平台就是访问不到。这不是代码一定写错了而是本地调试天然隔着一层网络边界。飞书平台的事件回调由云端服务器发起它不会运行在你的电脑上也无法直接访问你机器里的localhost、127.0.0.1或公司 Wi-Fi 下的内网 IP。再加上事件订阅通常要求公网可访问的 HTTPS URL本地 HTTP 服务就更不可能直接通过校验。这篇文章用一个最小回调服务把链路跑通本地启动 Node.js 或 FastAPI 接收飞书事件通过 cpolar 把本地端口映射成公网 HTTPS 地址再在飞书开放平台完成 URL 校验、事件回调配置和日志排查。它适合开发、联调和验收不要把临时隧道当成长期生产入口。为什么本地事件订阅总是调不通飞书事件订阅的调用方向很重要不是你的电脑去请求飞书而是飞书平台在发生事件后主动 POST 到你配置的回调 URL。比如用户给机器人发消息、群聊里新增成员、审批状态变化平台都会按你订阅的事件类型向目标地址推送 JSON 数据。如果目标地址写成下面这些基本都会失败http://127.0.0.1:3000/feishu/events http://localhost:3000/feishu/events http://192.168.1.23:3000/feishu/events原因很直接127.0.0.1对飞书服务器来说是飞书服务器自己不是你的电脑192.168.x.x是内网地址公网服务器通常无法路由进来普通 HTTP 地址也可能不满足平台对安全回调地址的要求。你本地自测成功只能说明服务在本机可用不能证明飞书云端能访问。cpolar 的作用就是补上“公网 HTTPS 入口”这一段。它在你的电脑和 cpolar 云端之间建立隧道外部访问https://xxxx.cpolar.top/feishu/events时请求会被转发到你本地的http://127.0.0.1:3000/feishu/events。这样飞书看到的是一个公网 HTTPS URL你本地仍然可以保留开发环境和日志。先写一个最小 Node.js 回调服务下面先用 Node.js 写最小服务。它包含三个能力健康检查、飞书 URL 校验、普通事件回调日志。为了便于理解示例先把签名校验写成函数开发时可以打开严格校验如果你还在做第一轮连通性排查也可以先只打印请求头和原始 body。新建目录并安装依赖mkdir feishu-cpolar-callback-demo cd feishu-cpolar-callback-demo npm init -y npm install express创建server.jsconst crypto require(crypto); const express require(express); const app express(); const PORT Number(process.env.PORT || 3000); const ENCRYPT_KEY process.env.FEISHU_ENCRYPT_KEY || ; app.use(express.json({ verify: (req, _res, buf) { req.rawBody buf.toString(utf8); } })); function safeCompareHex(a, b) { const left Buffer.from(a || , utf8); const right Buffer.from(b || , utf8); if (left.length ! right.length) return false; return crypto.timingSafeEqual(left, right); } function verifyFeishuSignature(req) { const timestamp req.header(X-Lark-Request-Timestamp) || ; const nonce req.header(X-Lark-Request-Nonce) || ; const signature req.header(X-Lark-Signature) || ; if (!ENCRYPT_KEY) { return { ok: true, reason: signature skipped in local demo }; } const base timestamp nonce ENCRYPT_KEY (req.rawBody || ); const expected crypto.createHash(sha256).update(base).digest(hex); const ok safeCompareHex(expected, signature); return { ok, reason: ok ? matched : signature mismatch }; } app.get(/health, (_req, res) { res.json({ ok: true, service: feishu-callback-demo, time: new Date().toISOString() }); }); app.post(/feishu/events, (req, res) { console.log(\n[feishu callback], new Date().toISOString()); console.log(headers:, { timestamp: req.header(X-Lark-Request-Timestamp), nonce: req.header(X-Lark-Request-Nonce), signature: req.header(X-Lark-Signature) }); console.log(body:, JSON.stringify(req.body, null, 2)); const body req.body || {}; if (body.type url_verification body.challenge) { return res.status(200).json({ challenge: body.challenge }); } const signature verifyFeishuSignature(req); if (!signature.ok) { console.warn(signature failed:, signature.reason); return res.status(401).json({ code: 401, msg: invalid signature }); } return res.status(200).json({ code: 0, msg: success }); }); app.listen(PORT, 127.0.0.1, () { console.log(local callback listening on http://127.0.0.1:${PORT}); });启动服务node server.js先在本机确认服务可用curl http://127.0.0.1:3000/health curl -X POST http://127.0.0.1:3000/feishu/events \ -H Content-Type: application/json \ -d {type:url_verification,challenge:local-test}第二个命令应该返回{challenge:local-test}这一步只证明本地服务逻辑没问题。要让飞书开放平台访问到它还需要公网 HTTPS 地址。如果你更习惯 FastAPIPython 项目可以用 FastAPI 起一个同样的最小回调服务。目录和依赖如下mkdir feishu-cpolar-fastapi-demo cd feishu-cpolar-fastapi-demo python -m venv .venv source .venv/bin/activate pip install fastapi uvicorn创建main.pyimport hashlib import hmac import json import os from datetime import datetime from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app FastAPI(titleFeishu Callback Demo) ENCRYPT_KEY os.getenv(FEISHU_ENCRYPT_KEY, ) def verify_feishu_signature(headers, raw_body: bytes) - bool: if not ENCRYPT_KEY: return True timestamp headers.get(x-lark-request-timestamp, ) nonce headers.get(x-lark-request-nonce, ) signature headers.get(x-lark-signature, ) base timestamp nonce ENCRYPT_KEY raw_body.decode(utf-8) expected hashlib.sha256(base.encode(utf-8)).hexdigest() return hmac.compare_digest(expected, signature) app.get(/health) async def health(): return {ok: True, service: feishu-callback-demo, time: datetime.now().isoformat()} app.post(/feishu/events) async def feishu_events(request: Request): raw_body await request.body() body json.loads(raw_body.decode(utf-8) or {}) print(\n[feishu callback], datetime.now().isoformat()) print(headers, dict(request.headers)) print(body, json.dumps(body, ensure_asciiFalse, indent2)) if body.get(type) url_verification and body.get(challenge): return {challenge: body[challenge]} if not verify_feishu_signature(request.headers, raw_body): return JSONResponse(status_code401, content{code: 401, msg: invalid signature}) return {code: 0, msg: success}启动uvicorn main:app --host 127.0.0.1 --port 3000Node.js 和 FastAPI 二选一即可。真实项目里可以把 URL 校验和事件处理放在同一路由因为飞书平台校验地址时也是向你配置的事件订阅 URL 发请求。用 cpolar 映射成本地 HTTPS 回调入口本地服务监听在127.0.0.1:3000后打开一个新的终端启动 cpolar 隧道cpolar http 3000启动后控制台会显示一个公网地址形态通常类似https://abcd-xxx.cpolar.top把事件回调路径拼上去就是要填到飞书开放平台的 URLhttps://abcd-xxx.cpolar.top/feishu/events在填到飞书之前先用外部 HTTPS 地址自测一次curl https://abcd-xxx.cpolar.top/health curl -X POST https://abcd-xxx.cpolar.top/feishu/events \ -H Content-Type: application/json \ -d {type:url_verification,challenge:cpolar-test}如果这里能返回cpolar-test同时本地终端能看到请求日志就说明“公网 HTTPS - cpolar - 本地端口”这条链路已经通了。后面飞书校验失败时就可以把问题缩小到平台配置、请求格式、签名或应用权限上而不是继续怀疑本地端口不可达。在飞书开放平台配置事件订阅进入飞书开放平台应用后台后常见配置路径是“事件订阅”或“事件与回调”。不同应用类型的菜单名称可能略有差异但核心步骤基本一样开启事件订阅能力。将请求地址填写为https://你的-cpolar-域名/feishu/events。保存时平台会发起 URL 校验请求。本地服务收到url_verification类型请求后原样返回challenge。校验通过后再选择需要订阅的事件例如接收消息、群聊事件、审批事件等。根据事件类型补齐应用权限并发布或重新安装应用。URL 校验最容易犯的错误是返回格式不对。飞书发来的请求体里会包含challenge字段你的服务需要返回 JSON{ challenge: 平台发来的 challenge 字符串 }不要返回字符串本身也不要包在data字段里。HTTP 状态码也应该是200。如果你的服务返回302、404、500或超时平台通常都会认为校验失败。另一个常见问题是路径不一致。你本地代码监听的是/feishu/events飞书后台就必须填写完整路径。如果只填 cpolar 根域名平台请求会打到/自然不会进入回调处理函数。签名、Challenge、状态码和日志怎么排查事件订阅调试按顺序排查不要一上来就改业务代码。第一步看 cpolar 隧道是否在线。访问/health能返回 JSON说明公网入口和本地端口是通的如果/health都失败先检查 cpolar 进程、本地服务端口和防火墙。第二步看本地终端有没有日志。飞书点击保存或重新校验时本地服务应该打印请求头和请求体。如果 cpolar 有访问记录但本地没有日志可能是本地端口写错了如果本地完全没有任何访问可能是飞书后台 URL 填错或者 cpolar 地址已经变更。第三步看challenge返回。URL 校验阶段不要做复杂业务处理收到typeurl_verification就立即返回challenge。校验请求不是普通事件不需要你把它当成消息处理。第四步看状态码。飞书事件回调通常希望你的服务快速返回成功响应。示例里普通事件返回200和{code:0,msg:success}这能避免平台反复重试。耗时任务不要在回调请求里同步执行太久应该先记录事件 ID 或消息 ID再交给队列、后台任务或异步流程处理。第五步看签名。生产环境一定要校验飞书签名避免任何人伪造请求打到你的回调地址。调试第一轮可以先打印请求头和 body确认字段存在后再打开严格校验。注意签名计算依赖时间戳、随机串、密钥和原始请求体如果中间件提前改写了 body或者你用格式化后的 JSON 重新计算就可能导致验签失败。第六步看事件权限。URL 校验通过不代表事件一定会推送。比如订阅“接收消息”事件后应用还需要对应权限并且机器人可能需要被拉入群聊或安装到指定范围。没有权限时平台不会按你预期推送事件或者推送内容字段不完整。安全收口临时入口不要长期裸奔cpolar 很适合开发联调但临时公网入口也意味着你的本地服务被外部网络访问到了。调试时至少做好这些收口只暴露必要路由不要把整个后台管理系统、数据库控制台、调试面板一起映射出去。本地服务监听127.0.0.1即可让 cpolar 转发到本机端口不需要额外监听0.0.0.0。事件回调必须校验飞书签名敏感操作还要做事件类型、应用 ID、租户信息和消息来源校验。日志里避免打印 access token、refresh token、用户手机号、邮箱、消息全文等敏感信息。联调结束后关闭 cpolar 隧道并从飞书后台移除临时回调地址或切回正式域名。不要把临时 cpolar 地址硬编码进仓库也不要把密钥写进示例代码提交到公开仓库。真实上线时事件回调最好部署到稳定的 HTTPS 服务上配合网关、鉴权、日志、告警和限流。cpolar 更适合“本地开发阶段快速让平台打进来”用来确认协议、字段、权限和业务处理逻辑。一个可复用的联调流程我实际调这类飞书回调时会按这个顺序做本地起最小服务只实现/health和/feishu/events。本机curl通过确认 challenge 返回格式正确。启动 cpolar拿到 HTTPS 地址。用 cpolar HTTPS 地址再curl一遍确认本地能看到日志。把完整事件订阅 URL 填到飞书开放平台。先让 URL 校验通过再订阅具体事件。触发真实事件例如给机器人发一条测试消息。根据本地日志检查请求头、事件类型、事件 ID、消息内容和响应状态码。打开签名校验确认验签通过。联调完成后关闭隧道把平台配置切回正式环境。这个流程的好处是每一步都有明确的判断标准。失败时你知道该看哪里而不是在飞书配置、本地代码、网络入口和权限之间来回猜。总结飞书机器人事件订阅本地调不通核心原因通常不是框架问题而是飞书云端无法直接访问你的本机地址。localhost、内网 IP、本地 HTTP 端口都不适合作为平台回调 URL。用 cpolar 可以快速生成一个公网 HTTPS 入口把飞书的 URL 校验和事件回调转发到本地 Node.js 或 FastAPI 服务。调试时重点关注四件事challenge必须按格式返回回调路径必须一致普通事件要快速返回200签名和权限要在链路通了之后逐步收紧。最后再强调一次边界cpolar 是开发联调工具不是让本地服务长期裸露在公网的理由。用它把协议、字段、权限和业务逻辑验清楚完成后关闭隧道正式环境再迁移到稳定的 HTTPS 服务上这样既能提高调试效率也能把安全风险控制在合理范围内。