飞书与腾讯会议API对接实战:从JWT签名到卡片消息推送
先说一下我为什么想写这个选题。做企业办公集成的朋友应该都遇到过这类需求群里喊一嗓子“今天会议纪要谁整理一下”过了半小时没人接话。尤其现在远程会议频率这么高腾讯会议开完一场会真正值钱的信息散落在聊天截图、白板照片、临时录制的片段里想翻出来做复盘非常痛苦。飞书和腾讯会议的对接本质上就是把腾讯会议这一侧产生的会议数据、纪要内容自动流转到飞书这一侧的群聊和云文档里让两个系统自己把会后的动作干完而不是靠人肉搬运。这个需求在市面上没有现成的开箱即用方案大多数团队只能走二次开发。我前前后后帮三个团队搭过类似的通道踩了不少坑把完整流程和排查心得整理出来给打算自己动手做的朋友一个参考。内容会覆盖两个开放平台的权限准备、关键接口调用、消息卡片发送、常见错误码排查你们照着做基本能跑通一条最小可用的链路。1. 对接前先想清楚两个平台能交换什么1.1 三个最常用的对接场景在动手写代码之前务必要先把业务场景拆清楚因为不同场景决定了你调用哪一类接口、用拉取还是回调、数据放群聊还是放文档。场景一会后数据巡检。常见于销售团队、项目复盘场景。管理者需要了解每天开了多少场会、单场时长、哪些人参加了、缺席了哪些人。这种需求适合用定时任务每天早上或每周一拉取上一周期的会议报告推送到飞书群里。实现周期短逻辑简单容错率也高。场景二会议结束即时通知。管理者希望重要客户会议一结束第一时间在飞书群里看到摘要卡片包括会议主题、起止时间、参会人数、会议号等。这种需求适合用腾讯会议的 webhook 回调监听 meeting_end 事件实时性比定时拉取好得多。代价是你需要有一个公网可访问的回调地址以及处理回调重试的幂等逻辑。场景三纪要文件归档。腾讯会议现在有 AI 纪要也有本地录制文件但这些内容默认躺在腾讯会议侧想进入飞书云文档体系还得靠手工下载再上传。对接后可以做自动化归档把会议纪要转成文档存到飞书云空间再把链接卡片发到群里同事点开就能看。这三个场景不是互斥的很多团队会组合使用。但我不建议第一个版本就把所有功能都做上先从场景一或场景二选一个跑通验证数据链路没问题再叠加其他能力。1.2 权限分层不是所有账号都能对接对接踩坑的第一道坎往往是权限。飞书和腾讯会议都是企业级产品开放 API 对账号类型有明确限制。腾讯会议这一侧个人免费版基本没有开放接口的能力部分查询类接口即使能调用也拿不到完整数据。要做系统对接至少是商业版或企业版账号并且需要在腾讯会议开放平台创建应用拿到 SecretId 和 SecretKey。这里有个容易忽略的点即使你有企业版账号如果管理员没有给你开通“开放平台”权限你依然创建不了应用会在第一步卡住。飞书这一侧相对好一些个人创建的企业自建应用也能用但发送消息、读写云文档这些敏感接口需要企业管理员审批。如果公司对权限管控严格建议提前跟管理员沟通申请固定的权限组合别等代码写完了才发现某个文档权限没批。一句话总结对接之前先确认自己手里有没有腾讯会议企业版账号、飞书管理员审批权限这两个基础条件否则流程走一半会很尴尬。2. 飞书侧准备机器人、应用凭据与权限2.1 先分清机器人类型避免选错飞书生态里“机器人”这个词其实指两类完全不同的东西很多新手在这里混淆。第一类是自定义机器人也叫 webhook 机器人。你在飞书群设置里添加一个机器人拿到一个 webhook 地址往这个 URL POST 一段 JSON 就能往群里发消息。优点是配置极其简单不需要写代码就能拿到地址适合快速验证。缺点是只能往固定的群发无法读取群信息也无法按人发送私聊消息更谈不上读取云文档。第二类是应用机器人也叫自建应用机器人。你在飞书开放平台创建一个企业自建应用启用“机器人”能力后应用便拥有一个机器人身份可以通过 API 往任意群、任意用户发送消息还能申请云文档读写权限、读取用户信息等。对接腾讯会议场景我建议直接上应用机器人。原因很现实你要发送的往往不只是纯文本还要携带参会人表格、会议详情链接、后续操作按钮。自定义机器人尽管也能发卡片但扩展性远不如应用机器人而且后续你要接 AI 摘要、自动建文档都需要应用身份调用飞书 API。2.2 创建自建应用并拿到敏感凭据打开飞书开放平台用企业管理员账号登录进入开发者后台创建一个企业自建应用。创建完成后重点看两个地方第一“凭证与基础信息”页面这里放着 App ID 和 App Secret这是整个对接过程中最重要的两个字符串。App ID 相当于你的应用账号App Secret 相当于密码用来换取访问令牌。注意 App Secret 只在创建时完整展示一次后续只能重置务必保存到安全的地方。第二“权限管理”页面按你的业务需求开通 API 权限点。发送群消息要开通im:message:send_as_bot读写云文档要开通docx:document系列权限如果想按用户维度查信息还要申请contact:user.base:readonly。权限点申请后需要发布版本让管理员审批审批通过才会生效。这里分享一个我踩过的坑权限点光申请没用必须发布一个新版本才会真正生效。我第一次只点了申请以为权限立刻就有了结果调用接口一直报“无权限”排查了半天才发现是版本没发布。2.3 获取访问令牌的关键代码飞书开放接口的认证方式是 OAuth 风格调用业务 API 前需要先拿 tenant_access_token。所谓 tenant_access_token就是“企业租户身份”的访问令牌适合机器人和服务端场景不需要具体某个员工扫码授权。import requests APP_ID cli_xxxxxxxxxxxxxxxx APP_SECRET xxxxxxxxxxxxxxxxxxxxxxxx url https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal payload {app_id: APP_ID, app_secret: APP_SECRET} resp requests.post(url, jsonpayload) data resp.json() if data.get(code) 0: token data[tenant_access_token] print(token) else: print(获取失败:, data)返回结构里 token 的有效期大概是两小时实际测试一般接近这个时长。我建议在服务端缓存 token别每次请求都重新获取。第一是避免触发频控第二是很多页面接口并发起来之后如果每个请求都重新申请token 会瞬间失效反而导致批量失败。缓存方案可以简单粗暴本地存一个文件或 Redis带过期时间到期前五分钟刷新。3. 腾讯会议侧准备开放接口与鉴权3.1 企业版和 API 的对应关系腾讯会议的开放平台入口和飞书不一样需要单独登录。如果你在腾讯会议官网找不到开放平台入口可以试试在控制台设置里找“开放平台”或“开发者选项”不同版本的界面差异比较大。目前在用的腾讯会议 API 有两个版本。老版本 V1 接口比较有限新版本 V2 接口更全域名是api.meeting.qq.com文档也是围绕 V2 展开的。我下面的示例都基于 V2。企业版账号登录开放平台后创建应用会拿到 SecretId 和 SecretKey。SecretId 相当于用户名会出现在请求头里SecretKey 相当于密码只用于本地签名计算绝不外传。这里要特别强调腾讯会议的鉴权方式不是简单地传一个 App Secret而是用 JWT 签名签名的密钥就是你自己的 SecretKey服务端用 SecretId 查找对应公钥来验签。3.2 JWT 签名最常见的一道坎腾讯会议 API 的请求头需要两个字段X-TC-Key放 SecretIdX-TC-Token放 JWT。JWT 的 payload 里至少要包含四个声明appid、secret_id、iat签发时间、exp过期时间。签名算法是 HS256密钥是 SecretKey。import time import jwt secret_id your_secret_id_here secret_key your_secret_key_here app_id your_app_id_here payload { appid: app_id, secret_id: secret_id, iat: int(time.time()), exp: int(time.time()) 600, } token jwt.encode(payload, secret_key, algorithmHS256) headers { X-TC-Key: secret_id, X-TC-Token: token, Content-Type: application/json, } print(token)写签名代码时有三个细节需要注意。第一exp 不能设置太长官方建议不超过 10 分钟实际我习惯设置 5 到 10 分钟够用就行太长反而增加安全风险。第二payload 里的secret_id是明文放进去的和请求头的 X-TC-Key 一致看似重复但这是协议要求别漏了。第三如果用的是 PyJWT 旧版本生成的 token 可能不兼容建议固定库版本我测试用的是 PyJWT 2.x。3.3 先手动调通一个查询接口签名代码写完后不要急着上业务逻辑先用 curl 手动调一个查询接口验证整条链路。我最常用的是“获取会议参会成员列表”接口因为开会是高频行为数据也直观。curl -X GET \ https://api.meeting.qq.com/v1/meetings/{meeting_id}/participants?page1page_size20 \ -H X-TC-Key: your_secret_id \ -H X-TC-Token: your_jwt_token \ -H Content-Type: application/json把{meeting_id}替换成真实会议号如果返回 JSON 里包含 participants 数组说明签名和权限都正常。这里有一个常见误区腾讯会议接口返回的时间戳大部分是毫秒少数接口是秒我遇到过用秒去解析导致时间偏差八个小时的案例写解析代码时一定要先确认字段类型。手动调通的另一个好处是能看清返回结构。比如参会人信息里可能同时包含 userid企业内部用户 ID、ms_open_id腾讯会议开放 ID和 user_name不同字段用途不同。你推送到飞书群时用的是 user_name但要做数据关联分析时需要用 ms_open_id。4. 端到端落地从会议数据到飞书群4.1 定时任务拉取数据并发送表格消息先实现一个最小闭环定时拉取腾讯会议数据拼接成表格卡片发送到飞书群。这里我用 Python 写一个简化版核心逻辑分三步。第一步调腾讯会议接口拿最近会议列表。第二步逐个会议拉参会人。第三步把数据拼成飞书卡片发出去。import json import time import jwt import requests # 飞书配置 FEISHU_APP_ID cli_xxxxxxxxxxxxxxxx FEISHU_APP_SECRET xxxxxxxxxxxxxxxxxxxxxxxx FEISHU_CHAT_ID oc_xxxxxxxxxxxxxxxx # 群聊 ID # 腾讯会议配置 TC_APP_ID your_app_id TC_SECRET_ID your_secret_id TC_SECRET_KEY your_secret_key def get_feishu_token(): url https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal resp requests.post(url, json{app_id: FEISHU_APP_ID, app_secret: FEISHU_APP_SECRET}) return resp.json()[tenant_access_token] def get_tencent_header(): payload { appid: TC_APP_ID, secret_id: TC_SECRET_ID, iat: int(time.time()), exp: int(time.time()) 600, } token jwt.encode(payload, TC_SECRET_KEY, algorithmHS256) return { X-TC-Key: TC_SECRET_ID, X-TC-Token: token, Content-Type: application/json, } def send_card(chat_id, token, title, content): url https://open.feishu.cn/open-apis/im/v1/messages?receive_id_typechat_id card { config: {wide_screen_mode: True}, header: {template: blue, title: {tag: plain_text, content: title}}, elements: [ {tag: div, text: {tag: lark_md, content: content}}, {tag: hr}, {tag: note, elements: [{tag: plain_text, content: 数据来源腾讯会议 API 自动采集}]} ], } body { receive_id: chat_id, msg_type: interactive, content: json.dumps(card, ensure_asciiFalse), } headers {Authorization: fBearer {token}, Content-Type: application/json} return requests.post(url, headersheaders, jsonbody).json() feishu_token get_feishu_token() tc_headers get_tencent_header() # 示例查询最近一场会议的参会人实际运行请替换为真实会议 ID meeting_id your_meeting_id participants_resp requests.get( fhttps://api.meeting.qq.com/v1/meetings/{meeting_id}/participants?page1page_size20, headerstc_headers, ).json() names [item.get(user_name, ) for item in participants_resp.get(participants, [])] table_content | 会议ID | 参会人 |\n| --- | --- |\n table_content f| {meeting_id} | {, .join(names)} |\n result send_card(feishu_token, FEISHU_CHAT_ID, 腾讯会议数据日报, table_content) print(result)注意上面的代码为了演示做了大量简化生产环境需要加入异常处理、token 缓存、分页拉取等能力。但核心链路就是这三个函数剩下的都是工程化打磨。4.2 用卡片消息替代纯文本的四个理由第一次做完这个功能我直接用文本消息把数据发到群里结果第二天就被同事吐槽“满屏乱码感”。后来我把文本消息全部改成 interactive 卡片消息效果好了很多。第一个理由是排版。文本消息里的空格和对齐在手机上经常错乱表格更是一团糟。卡片消息支持 Markdown 和表格布局列对齐是固定的阅读体验完全不同。第二个理由是信息层级。卡片有 header、divider、note 这些组件可以把标题、正文、辅助信息分开群成员一眼扫过去就能抓到重点。纯文本基本没有层级可言。第三个理由是交互能力。卡片上可以挂按钮比如“查看会议详情”“打开纪要文档”点一下直接跳转腾讯会议或飞书文档。这对业务方来说是实实在在的效率提升。第四个理由是订阅与提醒。飞书卡片支持后续更新会议状态变更时你可以调用接口更新同一张卡片而不是重新发一条消息。比如会议从“进行中”变成“已结束”卡片内容可以原地变化这在项目复盘中非常实用。想用卡片里的表格展示更规范的话可以这样写 content 字段{ tag: div, text: { tag: lark_md, content: | 会议主题 | 开始时间 | 参会人数 |\n| --- | --- | --- |\n| 周例会 | 2025-01-13 10:00 | 12 | } }飞书卡片对 Markdown 表格的支持在 PC 端体验最好手机端也能正常展示只是列数太多时需要横向滑动。所以群里表格建议最多不超过五列内容太宽就裁剪。4.3 会议纪要自动上传飞书云文档发卡片只是第一步更深层的需求是归档。很多团队希望腾讯会议的 AI 纪要和录制文件能自动进入飞书云文档体系。腾讯会议侧如果接口支持直接拉取 AI 纪要那就走接口如果不支持退而求其次的办法是把本地导出的 docx 或 txt 文件通过上传接口传到飞书云空间。飞书创建云文档的接口是POST /open-apis/docx/v1/documents请求头带上 tenant_access_token创建一个空文档后拿到 document_id。然后可以用追加内容的方式把纪要写入。这里顺带回答一个很多人在社区问的问题“dify 首次使用飞书云文档的授权凭证如何取得”逻辑其实和这里完全一样——你需要在飞书开放平台创建一个自建应用获取 App ID 和 App Secret 作为授权凭证并且在权限管理里开通对应的云文档读写权限。没有什么捷径凭证就是自建应用的那两个字符串。Dify 或者你自己写的脚本拿同一套凭证去换 token、调接口本质都一样。上传完成后飞书文档会生成一个 URL格式类似https://xxx.feishu.cn/docx/{document_id}。把这个链接塞进卡片消息同事点开就是完整纪要。我实际用下来这个功能比直接往群里贴长篇文本受欢迎得多毕竟没人想在聊天窗口里读两万字纪要。5. 常见问题与排查技巧实录5.1 腾讯会议错误码速查表对接过程中最耗时间的往往是错误码排查。我整理了一份高频错误码对照表都是实际踩过的错误码含义处理方法1101未授权访问检查 SecretId 是否填写正确检查应用是否处于正常状态1102接口权限不足确认企业版本是否支持该接口需要商业版或企业版400参数错误按文档检查请求参数特别是会议 ID 和时间戳格式429请求过于频繁降低调用频率加入退避重试策略10007会议不存在确认 meeting_id 是数字型 ID 还是字符串型 ID两者不能混用遇到 1101 和 1102 时先不要怀疑代码去控制台确认应用详情和接口文档里的权限说明。腾讯会议的权限是按接口维度分配的看起来很像“已授权”实际上可能只是部分接口可用。5.2 飞书消息发不出去从三个方向排查飞书接口报错时返回体里会带 code 和 msg排查思路相对固定。我列三个最常见的场景。场景一返回错误码 99991663提示无权限。九成是权限点没开通或者版本没发布。先去开发者后台的权限管理看对应权限是否已通过审批再到版本管理中确认最新版本是“已发布”状态而不是“审核中”。场景二返回错误码 10003提示群不存在。多半是你把 chat_id 填错了。飞书的 chat_id 不是群名也不是群链接里的那串数字而是通过接口查询出来的群聊标识。可以通过GET /open-apis/im/v1/chats拉取当前应用可访问的群列表确认。场景三消息发送成功但群里看不到。这种情况比较诡异我碰到过两次最后都发现是发送到了机器人的“自聊”会话或者被群主设置了全员禁言。先确认接收方是真实群聊再看群设置里是否屏蔽了机器人消息。5.3 摄像头、时区、幂等这些周边问题搜索结果里有人问“腾讯会议不能使用电脑自带摄像头吗”这个问题虽然和 API 对接无关但我在帮团队做集成时也遇到过。如果开会时摄像头是灰的先看腾讯会议客户端“设置-视频”里的摄像头设备是否选择了正确型号再看操作系统隐私设置里是否允许腾讯会议访问摄像头。这两个地方没问题基本就能解决。它不影响 API 对接但如果你同时负责团队 IT 支持大概率会被问一遍。时区问题则实实在在影响对接质量。腾讯会议 API 返回的时间戳是毫秒级的 Unix 时间戳飞书消息展示时默认按北京时间转换但如果你在服务器上跑脚本服务器的时区可能不是 UTC8需要显式指定时区转换否则推送的会议时间会差八个小时。幂等处理也要重视。腾讯会议 webhook 回调是会重试的同一场会议结束的事件可能推送多次。如果回调处理函数里直接发消息群里就会重复刷卡片。我的做法是维护一个本地缓存用会议 ID 加事件类型作为唯一键处理过的直接丢弃。5.4 webhook 回调的调试技巧如果你选择了场景二需要接收腾讯会议的 webhook 回调调试阶段有个小技巧先用本地工具把回调地址暴露到公网临时测试。正式环境建议把回调地址放在网关后面加上签名校验和 IP 白名单。回调处理函数要保持轻量只做事件的解析和消息推送重活放到异步队列里。万一推送飞书失败了要记录原始事件方便后面手工补发。# 伪代码展示回调处理的基本骨架 from flask import Flask, request app Flask(__name__) app.route(/tencent/callback, methods[POST]) def callback(): data request.json # 校验签名逻辑省略线上务必实现 event data.get(event, {}) if event.get(meeting_ended): # 异步处理拉取参会人、发送飞书卡片 pass return {code: 0, message: ok}回调地址建议使用 HTTPS腾讯会议控制台可以配置回调 URL 模板测试时先用最简单的路径跑通后再设计路由和鉴权。6. 通用经验一次对接复用三类系统做完飞书和腾讯会议的对接我最大的体会是这种“两个企业 SaaS 之间打通数据”的需求在国内办公生态里太常见了。腾讯会议换成钉钉会议或 Zoom飞书换成钉钉或企业微信套路几乎一模一样。核心方法论就三条。第一务必先把两个平台的权限边界摸清楚哪些接口需要什么版本、什么权限先列一个清单再动手。第二先用最笨的方式手动调通接口再写代码封装不要一上来就设计复杂的调度框架。第三消息推送只是表面真正有价值的是后续的数据归档和查询分析能力。最后分享一个小技巧是我在实际使用中收到同事反馈最多的一点卡片消息不要每次都重新生成新消息而是尽量复用同一条消息做状态更新。比如会议从等待开始变为进行中、再变为已结束卡片上的状态和内容随事件刷新群里始终只保留一条完整记录。这种体验在项目复盘时尤其舒服翻聊天记录不用在几十条重复通知里来回找。如果你也想在团队里做类似的对接先拿一个最小场景跑通整条链路再逐步加功能。第一次可能花掉两三天但后面每次新增场景基本只需要半天。这套思路我前前后后用了好几轮稳定可靠。