ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

飞书与腾讯会议对接实战:事件回调、消息卡片与自动化通知

飞书与腾讯会议对接实战:事件回调、消息卡片与自动化通知 上周我正盯着飞书群里的长聊天气泡同事丢进来一个腾讯会议链接说“五分钟后评审开始”。结果消息被几十条讨论淹没真正进会议的人不到一半前十分钟全在等人。这大概就是多平台办公最典型的痛点飞书负责沟通协调腾讯会议负责视频会话两个工具各自好用但割裂开了信息全靠人工搬运。我花了大概三天时间把飞书和腾讯会议做了一次系统对接让会议状态自动同步到群里也支持在飞书里直接发起腾讯会议。这篇内容就是整个对接实践的完整记录适合正在做内部效率工具的企业IT、后端开发以及被跨平台会议折磨的运营同学参考。1. 为什么要做这个对接两张皮带来的实际成本飞书和腾讯会议的共存在很多公司里不是“二选一”而是“都装”。飞书的文档协作、IM群组、审批流做得深腾讯会议的音视频能力和入会体验更成熟销售、研发、HR各个团队都有自己的使用习惯。可是只要一天不打通一线员工就要手动完成一套重复动作先在腾讯会议客户端建好会议再切到飞书群复制链接艾特所有人等到会议临近还要喊第二遍。这还不是最烦的。真正麻烦的是会议状态的不可知性。发起人临时取消了会议群里没人知道大家到点还傻等会议开始时间改了旧卡片还在原位置挂着导致有人按错误时间入会会议结束后需要整理结论和待办又要找聊天记录重新拼一遍。这些看起来是小事放大到一周几十场会议就是在持续消耗团队注意力。所以这个对接的本质不是一个炫技的API调用练习而是把“腾讯会议的会议生命周期事件”和“飞书的IM通知能力”组合起来。简单说腾讯会议侧出事件飞书侧出消息渠道中间由一个中转服务做转发和适配。这样一旦会议被创建、修改、取消、开始、结束群里都能第一时间收到结构化的通知卡片而不是靠人肉提醒。另外还有一个隐性收益对接完成之后会议数据第一次变得可以被统计了。谁开的会、什么主题、有没有按时开始、平均时长多少这些数据落在飞书文档或多维表格里团队做周报和月度复盘时就再也不用拍脑袋。2. 对接前的账号体系与权限准备动手写代码之前先把两个平台的开放能力摸清楚这一步决定后面顺不顺畅。整个对接涉及三套账号体系飞书开发者后台、腾讯会议开放平台以及你自己的服务器环境。任何一个地方的权限没开对联调时都有可能卡住。2.1 飞书侧先建一个应用别用自定义机器人硬扛对接飞书有两种常见方式一种是往群里加一个“自定义机器人”靠Webhook地址发消息另一种是在飞书开放平台创建一个企业自用应用通过API发消息并接收事件回调。我做这次对接时选择了后者原因是自定义机器人只能单向推送没法接收用户在飞书里的指令而且签名校验、消息卡片能力都受限。创建应用时建议记住这几个关键值App ID应用的唯一标识调用API时要用App Secret应用的密钥用来换tenant_access_tokenEncrypt Key回调事件加密用的如果开启加密必须配置Verification Token回调验证令牌校验请求合法性创建完应用之后需要在“权限管理”里开通至少这几个权限发送群消息im:message、获取群信息im:chat、读取用户信息contact:user.base:readonly。这些权限不是审批完立刻生效一般要等几分钟遇到“权限不足”报错别急着怀疑代码先检查权限是否已经生效。2.2 腾讯会议侧企业自建应用要走完主体认证腾讯会议的开放平台逻辑比较有意思个人开发者能调用的接口极其有限想拿到创建会议、查询会议这类核心API必须走企业认证以企业主体创建自建应用。这一步容易忽略很多人卡在注册环节其实是主体认证没有完成。应用创建成功后你会拿到一组三件套AppID应用唯一标识SecretID鉴权身份IDSecretKey签名密钥腾讯会议的鉴权方式发生过好几次迭代。我这次用的是JWT方式用AppID、SecretID做身份声明用SecretKey做HMAC-SHA256签名生成JWT后再换取access_token调用创建会议等OpenAPI时需要把它放在Authorization头里。细节以官方开放平台当下的文档为准但核心思路不变SecretKey绝不能出现在前端代码或者日志里必须放在服务端环境变量中。2.3 中转服务选型为什么必须有一层中间服务飞书不能直接请求腾讯会议腾讯会议的回调也不可能直接写进飞书所以必须有一个中转服务夹在中间。这个服务不复杂本质是一个HTTP Server接收腾讯会议的回调调用飞书API发消息同时接收飞书发来的指令去调腾讯会议API。技术栈我选的是Python FastAPI原因是事件处理逻辑不复杂FastAPI的async特性处理并发回调很轻松配合Redis做幂等和token缓存就够了。如果你团队主力是Node.js或Java用Express或Spring Boot也没问题关键不在语言在于事件处理流程要清晰。部署上有个细节建议回调地址必须是公网可访问的HTTPS地址。开发和联调阶段可以用公网映射工具把本地服务暴露出去但正式使用强烈建议部署到云函数、容器服务这类固定域名的环境否则回调地址一变所有配置都要跟着改。密钥统一放到环境变量或密钥管理服务里不要写进代码仓库这一点在对接多个系统时尤其重要。3. 核心链路一腾讯会议事件触达飞书群这条链路是这次对接的基础解决的是“会议状态变化怎么自动通知到飞书群”的问题。整体流程是腾讯会议后台配置回调地址会议发生创建、取消、开始、结束等事件时腾讯会议向中转服务发一个POST请求中转服务解析事件后组装成飞书消息卡片再调用飞书API发到指定群。3.1 腾讯会议回调地址的配置与校验在腾讯会议开放平台的自建应用里找到“事件通知”或“回调配置”入口填入中转服务的回调URL比如https://your-domain.com/webhook/tencent-meeting。保存时平台通常会发一个校验请求你得让服务正确响应校验参数才能通过配置。回调服务的第一道工序是基础校验。腾讯会议的回调请求一般带有应用身份信息我实现时要求请求头或body中必须携带配置好的AppID和一个自定义token字段校验不通过的直接丢弃。这个校验不能省否则任何人往你的回调地址发POST都能触发飞书群消息白嫖事小被恶意刷屏影响全员办公事大。校验通过后把事件消息落一条日志再进入处理逻辑。日志格式我建议至少包含会议ID、事件类型、时间戳、原始body四个字段后面排查线上问题全靠它。3.2 组装飞书消息卡片文本消息和卡片消息的区别飞书机器人发消息有两种最常见类型文本消息和消息卡片。一开始我图省事直接用文本消息格式是这样的{ msg_type: text, content: { text: 会议「需求评审」已开始 } }文本消息能跑通但体验很差。会议标题、开始时间、会议号、入会链接全挤在一行手机端显示得密密麻麻入会链接长了还会被截断。后来我全部改成了interactive消息卡片用分栏和按钮把信息结构化效果完全不一样。卡片消息的content是一个JSON字符串核心结构如下{ config: { wide_screen_mode: true }, header: { title: { tag: plain_text, content: 会议通知 }, template: blue }, elements: [ { tag: div, text: { tag: lark_md, content: **会议主题** 需求评审\n**开始时间** 2025-03-10 14:00\n**会议号** 123456789 } }, { tag: action, actions: [ { tag: button, text: { tag: plain_text, content: 加入会议 }, type: primary, url: https://meeting.tencent.com/dm/xxxx } ] } ] }卡片的好处是信息层级清楚成员在群里扫一眼就知道什么会、什么时候开、怎么参加。按钮还能直接跳转不用再去复制链接。需要注意content字段传的是字符串而不是对象我第一次就是在这里报了序列化错误排查了半天才发现是类型问题。3.3 中转服务转发逻辑的完整实现把上面的流程串起来中转服务的核心代码大致是这样的import json import time import hmac import hashlib import base64 import requests from fastapi import FastAPI, Request app FastAPI() FEISHU_API https://open.feishu.cn/open-apis TENCENT_MEETING_CALLBACK_TOKEN your-custom-token # 飞书消息卡片发送 def send_feishu_card(chat_id: str, card_content: str): token get_tenant_access_token() url f{FEISHU_API}/im/v1/messages?receive_id_typechat_id headers { Authorization: fBearer {token}, Content-Type: application/json } body { receive_id: chat_id, msg_type: interactive, content: card_content } resp requests.post(url, headersheaders, jsonbody) return resp.json() app.post(/webhook/tencent-meeting) async def tencent_meeting_callback(request: Request): data await request.json() # 基础校验 if data.get(custom_token) ! TENCENT_MEETING_CALLBACK_TOKEN: return {code: 403, message: invalid token} event_type data.get(event_type) meeting_info data.get(meeting_info, {}) # 组装卡片 card { config: {wide_screen_mode: True}, header: { title: {tag: plain_text, content: f会议{event_mapping.get(event_type, 状态变更)}}, template: blue }, elements: [ {tag: div, text: {tag: lark_md, content: f**主题** {meeting_info.get(topic)}}}, {tag: div, text: {tag: lark_md, content: f**时间** {format_time(meeting_info.get(start_time))}}}, {tag: div, text: {tag: lark_md, content: f**会议号** {meeting_info.get(meeting_id)}}} ] } send_feishu_card(chat_id, json.dumps(card)) return {code: 0}这段代码省略了token获取和event_mapping的定义核心逻辑就是三步接收回调、组装卡片、发送飞书消息。实际生产环境还需要把chat_id和event_type的映射关系做成配置表比如technical-team群绑定研发例会management群绑定管理层会议不要全部塞进一个群。4. 核心链路二从飞书指令反向创建腾讯会议第一条链路是“腾讯会议发生什么飞书被动接收”相当于只做了单向同步。但实际用起来我发现很多同事更想要的是反向操作直接在飞书群里发一条指令机器人自动在腾讯会议后台创建一个会议然后把入会链接返回群里。这样就不用再切到腾讯会议客户端操作效率提升非常明显。4.1 飞书事件订阅接收用户在群里的指令要实现这个场景飞书侧要开启“事件订阅”能力让飞书把用户发给机器人的消息推送到中转服务。事件订阅的地址和腾讯会议的回调地址可以共用一个服务但路径不同比如/webhook/feishu。飞书回调的校验比腾讯会议严格很多。如果开启了Encrypt Key回调请求体里的encrypt字段是加密后的密文需要先用AES算法解密还要用请求头里的X-Lark-Signature配合timestamp做签名校验。我在这一步浪费了不少时间后来总结出一个关键点签名校验的timestamp必须用请求头里的值重新拼字符串不能用自己服务器的当前时间否则时区或时钟偏差会导致校验失败。解密和验签通过之后把消息里的文本提取出来做指令路由。我定义了几个简单的关键词规则消息以/meeting create开头解析后面的标题和开始时间消息包含机器人且包含“开会”或“建会议”走快捷创建流程只发了一个会议链接的解析链接并生成入会通知卡片指令解析的代码不复杂关键是容错要做好。同事发指令时不会严格按格式来少个引号多个空格都会导致解析失败所以每一条失败消息都要回一个友好提示指导用户怎么输入正确指令而不是静默丢弃。4.2 调用腾讯会议OpenAPI创建会议指令解析出标题和时间后中转服务下一步要调用腾讯会议接口创建会议。调用前必须先拿到access_token。获取token的接口需要带上AppID、SecretID、SecretKey生成的签名这一步严格按官方签名字典序排参计算排参顺序错了就直接401。创建会议的请求大致如下def create_tencent_meeting(topic: str, start_time: str, end_time: str, userid: str): token get_tencent_access_token() url https://api.meeting.qq.com/v1/meetings headers { Authorization: fBearer {token}, Content-Type: application/json } body { topic: topic, type: 1, # 预约会议 start_time: start_time, end_time: end_time, userid: userid, settings: { mute_enable: 1, allow_unmute_self: 0 } } resp requests.post(url, headersheaders, jsonbody) data resp.json() return data.get(meeting_info, {})这里有个特别容易踩的坑start_time和end_time要求的格式经常是时间戳字符串我第一次直接用了2025-03-10 14:00这种格式接口返回参数错误但错误信息很模糊。后来看了文档确认要Unix秒级时间戳才跑通。建议在调用前统一用time.mktime()或datetime.timestamp()转换。创建成功后返回的数据里包含会议号、入会链接、主持人密码这些字段要完整保存下来发给飞书群时全都要展示。主持人密码不要直接公开最好只提醒“会议主持人密码请在腾讯会议客户端查看”避免安全问题。4.3 会议卡片回推与参会人员提醒会议创建成功后中转服务会把会议信息组装成飞书卡片发回到发起指令的群。卡片里放三个核心动作加入会议、复制邀请链接、查看预定详情。{ elements: [ { tag: div, text: { tag: lark_md, content: **会议已创建**\n**主题** 需求评审\n**时间** 14:00-15:00\n**会议号** 123456789 } }, { tag: action, actions: [ { tag: button, text: {tag: plain_text, content: 加入会议}, type: primary, url: https://meeting.tencent.com/dm/xxxx } ] } ] }卡片发出之后还可以用飞书的im:messageAPI里的提醒功能把指令里提到的成员名转换成mention字段让接受提醒的人收到强通知。这个功能非常受同事欢迎因为普通群消息很容易被静音但被的成员手机上依然会弹出提醒。5. 踩坑实录联调中真实遇到的问题和排查链路写这篇记录时我最想分享的不是成功路上的那些代码而是联调阶段差点把我逼疯的几个问题。这些问题单独看都不难但如果不把排查思路讲清楚遇到一个就能卡你半天。5.1 回调地址的可用性本地联调时请求根本没进来第一个问题是内网穿透工具的稳定性。联调初期我把中转服务跑在自己电脑上用内网穿透映射出公网地址配到腾讯会议后台。现象是腾讯会议后台点“测试回调”总是失败但本地服务没有任何请求日志。排查链路是这样的先看腾讯会议后台的请求记录发现平台确实发起了请求再看本地日志发现根本没收到。这时基本能判断是网络链路问题。测试下来问题出在免费穿透服务的不稳定性上连接经常断而且免费域名还可能被平台风控拦截。后面换了有固定公网域名的测试服务器部署回调才开始稳定。这个坑的教训是从第一天起就用真实公网环境联调不要在本地穿透环境里耗时间。虽然穿透工具方便但SaaS平台的回调服务对目标地址的可达性要求很高免费的临时域名不适合作为调试基础环境。5.2 飞书回调验签的时间戳窗口第二个问题是飞书回调验签一直失败。现象是日志里sign校验不通过但用官方调试工具验证签名算法又是对的。排查链路比较曲折。先是怀疑签名算法实现有误反复比对文档把timestamp、nonce、encrypt拼来拼去还是没有解决。后来把请求头里飞书传过来的timestamp打印出来和服务器当前时间一比发现问题了飞书服务器和你本机的时间差超过了校验窗口通常是1小时。我本地电脑的时钟因为长时间未同步慢了十几分钟加上时区配置不对导致每次验签都被判成伪造请求。解决办法非常朴素先同步系统时钟再把验签代码里的时间比较改成使用请求头里的timestamp做容差判断而不是直接和本机时间死磕。之后还顺手给服务器配置了NTP自动校时一劳永逸。5.3 腾讯会议OpenAPI的参数格式与错误信息陷阱第三个问题是创建会议接口返回参数错误但错误信息只给了一个很泛的HTTP状态码不告诉你哪个字段错了。我逐字段排查后锁定在时间参数格式上。调用文档时一定先确认字段的数据类型和单位。比如腾讯会议要求的时间戳是秒级字符串而Java/JavaScript里经常拿到的Date.now()是毫秒级别差了1000倍直接传过去就会被判为非法时间。这类问题最容易出现在跨语言调用和跨平台对接中建议在中间服务里做一个统一的数据格式转换层入参出参都做约束避免每个业务方法都写一遍转换逻辑。另外中文标题在HTTP请求里也要注意编码。requests默认会处理但如果你自己拼URL或用了非标准HTTP客户端中文可能变成乱码。建议接口body统一走JSON不要用form-urlencodedParam也尽量控制在字母数字范围内。5.4 重复回调导致的通知风暴第四个问题是线上跑起来之后出现的一场会议状态变更群里收到了两三条相同的通知卡片。排查后发现两个来源一是腾讯会议平台本身可能对回调做了重试同一个事件推了多次二是飞书消息发送成功后如果不做幂等控制我们的服务在极端情况下也会重复触发发送逻辑。解决思路是在中转服务里增加幂等键判断。每个腾讯会议事件都有一个会议ID事件类型时间戳的组合把它作为幂等键存到Redis利用SETNX命令保证相同的事件只会处理一次。处理完成后设置一个过期时间比如10分钟这样既不影响正常处理又能挡住重复请求。import redis r redis.Redis(hostyour-redis-host, port6379, decode_responsesTrue) def is_duplicate(event_key: str) - bool: # SETNX成功返回True表示第一次处理已存在返回False说明重复 return not r.set(event_key, 1, nxTrue, ex600)这个方案在网上很多文章里都有提到但真正跑过才发现幂等键设计必须覆盖全事件类型。如果不同事件类型共用一个键会导致取消事件被开始事件顶掉通知丢失。按会议ID事件类型分别做键才是最稳的。6. 对接完成之后还能往哪些方向扩展两条核心链路跑通后这套系统的价值才刚开始显现。因为底层已经是“收到会议事件→加工→推到飞书”的架构扩展新功能就是在中间层加处理器不需要重新搭基础设施。6.1 自动会议纪要沉淀到飞书文档腾讯会议支持转写和录制但转写文本默认要在腾讯会议客户端里打开。对接完成之后可以在会议结束事件触发时自动拉取腾讯会议的转写文本或纪要结果转成飞书文档并把文档链接发到群里。这样会后群里的置顶就是一整个会议记录包状态卡片、参与人、结论文档、待办链接沉淀非常干净。调用腾讯会议获取转写文本需要单独申请接口权限开通流程比普通API长。如果暂时申请不下来也可以退而求其次会议结束后让机器人在群里发一条“请会议主持人上传会议纪要”的提醒卡片点击卡片直接创建飞书文档模板把模板链接发给群成员编辑。6.2 与飞书多维表格联动生成会议看板飞书多维表格的API能力很强可以把每次会议的标准信息主题、时间、主持人、参与人数、是否准时开始同步到一张会议看板里。运营或研发管理同学打开多维表格就能看到本周所有跨部门评审会议的进展还可以按主持人做透视统计。这个扩展的操作路径不复杂腾讯会议回调到达后除了发飞书群消息再调用多维表格API追加一条记录。字段建议至少包含日期、会议主题、主持人、会议号、状态、纪要链接。配置好以后周报数据直接从表格里拉不用再找行政要会议记录。6.3 给会议助手接上AI能力如果企业已经接了AI大模型应用可以把中转服务的消息处理层升级成智能体员工在飞书里发一段自然语言比如“帮我约明天下午三点到四点开个需求评审邀请产品和测试”机器人自动解析时间、主题、参与人创建腾讯会议后把邀请消息发给指定的飞书参与者。会议结束后还能让AI根据转写文本生成结构化结论和后续行动计划同步到飞书云文档。这里我自己的体会是跨平台对接最忌讳一上来就设计过于庞大的智能体。先把事件通知、会议创建这两条硬链路跑稳再逐步叠加AI能力出问题时排查范围才不会爆炸。最后留一个小建议。对接这类跨平台系统动手写代码之前先把两边的“事件模型”列成一张对照表比如腾讯会议的会议开始事件对应飞书的哪个群通知、哪个字段展示在哪一行。这个表就是系统的说明文档后面不管换人维护还是加新需求拿起来就能用比看代码省力得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进