1. 为什么要在飞书和腾讯会议之间搭一座桥
很多团队现在的协作状态是这样的:日常沟通、文档协作、审批流全在飞书里跑,但一到开会,尤其是跟外部客户或者跨部门的大型会议,大家还是习惯性地点开腾讯会议。结果就是——会议链接满天飞,会议纪要没人整理,待办事项散落在各个聊天窗口里,开完会等于失忆。
我所在的团队大概在半年前也是这个状态。飞书群聊里发一个腾讯会议链接,会开完了,录播文件在腾讯会议云端,聊天记录里的结论被刷屏淹没,谁负责什么全靠脑子记。后来我们花了两周时间做了一套飞书和腾讯会议的对接方案,核心目标就三个:会议自动创建并同步到飞书日历、会议录制和纪要自动回传到飞书群、会议待办自动生成飞书任务。
这套方案落地之后,最直观的变化是:开会前不用再手动复制会议号发群里,开会后五分钟内纪要链接和待办清单就自动出现在飞书群里。对于每天至少三场会的团队来说,省下来的时间相当可观。
这篇文章会从对接的整体架构设计讲起,然后拆解API鉴权的具体实现、会议生命周期事件的订阅与处理、录制文件与智能纪要的回传链路,最后分享几个我在实操中踩过的坑和对应的解决方案。适合有一定开发基础、正在考虑做飞书和腾讯会议集成的同学参考,也适合产品经理了解这套对接的可行性和边界。
提示:本文涉及的API调用均基于官方公开文档的通用实践,具体参数以你实际使用的版本为准。所有代码示例为逻辑示意,生产环境需要补充错误处理和重试机制。
2. 对接架构的选型与整体数据流设计
2.1 三种对接方式的对比与选择依据
在动手写代码之前,首先要搞清楚飞书和腾讯会议之间到底有哪些"连接点"。我调研下来,可行的对接方式主要有三种:
| 对接方式 | 实现原理 | 适用场景 | 维护成本 |
|---|---|---|---|
| 开放平台API直连 | 分别调用飞书开放平台和腾讯会议开放平台的REST API,在中间层做数据转换 | 需要深度定制、数据双向同步 | 中高 |
| Webhook事件订阅 | 订阅腾讯会议的会议事件回调,触发飞书侧的机器人消息或日历操作 | 会议状态变更通知、自动触发后续动作 | 中 |
| 第三方集成平台 | 使用Zapier、集简云等低代码平台做连接 | 快速验证、轻量级需求 | 低 |
我们最终选择了API直连+Webhook事件订阅的组合方案。原因很简单:第三方平台虽然上手快,但一旦涉及到自定义字段映射、复杂的条件判断、或者需要把会议数据写入飞书多维表格做统计分析,就会遇到天花板。而API直连虽然前期投入大一些,但后续的扩展性完全掌握在自己手里。
具体来说,我们的架构分成了四个模块:
- 会议创建模块:在飞书日历中创建日程时,自动调用腾讯会议API生成会议号和入会链接,回填到日程描述中。
- 事件订阅模块:通过腾讯会议的Webhook接收会议开始、结束、录制完成等事件,触发后续处理。
- 数据处理模块:将腾讯会议返回的录制文件、智能纪要等内容,转换成飞书消息卡片或云文档格式。
- 消息推送模块:通过飞书自建机器人,将处理后的内容推送到指定群聊或私聊。
2.2 数据流的完整链路拆解
整个数据流可以拆成两条主线:创建线和回传线。
创建线的流程是这样的:用户在飞书日历中新建一个日程,填写会议主题和时间。我们的中间服务监听到日历事件创建后,提取会议主题、开始时间、结束时间、参会人列表,调用腾讯会议的创建会议接口。腾讯会议返回会议号、入会链接、主持人密码等信息后,中间服务再调用飞书日历的更新接口,把这些信息写入日程描述,同时给参会人发送飞书消息通知。
回传线的流程稍微复杂一些:腾讯会议在会议结束后会触发录制完成事件,Webhook推送到我们的中间服务。中间服务根据事件中的会议ID,调用腾讯会议的文件列表接口获取录制文件和智能纪要的下载地址。然后调用飞书云文档的上传接口,把录制文件转存到飞书云空间,生成分享链接。最后通过飞书机器人,把会议主题、录制链接、纪要摘要、待办事项组装成一张消息卡片,推送到对应的飞书群。
这里有一个关键的设计决策:中间服务用什么样的部署方式。我们试过两种方案,一种是用飞书低代码平台搭建,另一种是用自建的Python服务。低代码平台的优势是开发快、不用管服务器,但劣势也很明显——处理大文件上传时容易超时,而且调试起来比较麻烦。最终我们选择了自建服务,用FastAPI做Web框架,部署在一台2核4G的云服务器上,日常几十场会议的并发完全够用。
2.3 鉴权体系的设计:双平台Token管理
飞书和腾讯会议的鉴权机制不太一样,需要分别处理。
飞书开放平台用的是tenant_access_token和user_access_token两套体系。tenant_access_token是以应用身份调用API,适合机器人发消息、上传文件这类操作;user_access_token是以用户身份调用,适合访问用户的日历、云文档等个人资源。tenant_access_token的有效期是2小时,需要在过期前刷新。
腾讯会议开放平台用的是AppId+SecretKey换取access_token的方式,有效期也是2小时。另外,腾讯会议的Webhook回调需要验证签名,签名算法是基于时间戳和密钥的HMAC-SHA256。
我们的做法是在中间服务里维护一个Token管理器,用定时任务在Token过期前30分钟自动刷新,同时把Token缓存在内存里,避免每次API调用都去请求新Token。这里有个细节:飞书的tenant_access_token刷新时,旧的Token会立即失效,所以如果你的服务是多实例部署的,需要用一个共享的缓存(比如Redis)来存储Token,否则会出现实例A刷新了Token,实例B还在用旧Token导致鉴权失败的情况。
# Token管理器的简化逻辑示意 import time import redis import requests class TokenManager: def __init__(self, app_id, app_secret, redis_client): self.app_id = app_id self.app_secret = app_secret self.redis = redis_client self.cache_key = f"feishu_token:{app_id}" def get_token(self): token = self.redis.get(self.cache_key) if token: return token.decode() # 请求新Token resp = requests.post( "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", json={"app_id": self.app_id, "app_secret": self.app_secret} ) data = resp.json() token = data["tenant_access_token"] # 缓存时预留300秒缓冲 self.redis.setex(self.cache_key, 7200 - 300, token) return token注意:腾讯会议的access_token刷新时,旧Token不会立即失效,但为了保持一致性和安全性,建议同样采用提前刷新的策略。
3. 会议创建环节的API调用与字段映射
3.1 飞书日历事件到腾讯会议参数的转换逻辑
飞书日历的事件结构和腾讯会议的创建会议接口,字段并不是一一对应的,需要做一层映射。我整理了我们实际用到的字段对照表:
| 飞书日历字段 | 腾讯会议字段 | 转换逻辑 |
|---|---|---|
| summary(日程标题) | subject(会议主题) | 直接映射,超过40字符截断 |
| start_time.timestamp | start_time | 时间戳直接传递 |
| end_time.timestamp | end_time | 时间戳直接传递 |
| description | - | 不映射,用于存放会议号回填 |
| attendee_ability | - | 不映射 |
| - | password | 自动生成6位数字密码 |
| - | auto_record | 根据会议类型判断,内部会议开启 |
这里有一个容易忽略的点:腾讯会议的创建会议接口要求传入的是秒级时间戳,而飞书日历返回的是毫秒级时间戳。如果不做转换,会议时间会变成几万年后,这个坑我踩过,排查了半天才发现是单位问题。
另外,腾讯会议的会议主题有长度限制,超过40个字符会报错。我们的处理方式是:如果飞书日程标题超过40字符,截取前37个字符加"...",同时在会议描述里保留完整标题。
3.2 参会人同步的两种策略:邀请制与密码制
腾讯会议的参会人管理有两种模式:一种是邀请制,需要把参会人的手机号或邮箱添加到会议邀请列表里;另一种是密码制,任何人只要有会议号和密码就能加入。
我们一开始用的是邀请制,把飞书日程里的参会人邮箱同步到腾讯会议的邀请列表。但很快发现一个问题:外部联系人没有飞书账号,也就没有邮箱,导致外部客户收不到会议邀请。后来改成了混合策略:
- 内部参会人:通过邀请制添加,确保他们能在腾讯会议客户端直接看到会议。
- 外部参会人:不添加到邀请列表,而是把会议号和密码通过飞书消息发送给会议组织者,由组织者自行转发。
这个策略的调整让外部会议的创建成功率从60%提升到了接近100%。如果你也在做类似的对接,建议一开始就考虑外部参会人的场景,不要等到上线后才发现问题。
3.3 会议号回填与日历更新的时序问题
创建完腾讯会议后,需要把会议号、入会链接、密码回填到飞书日历的日程描述里。这里有一个时序问题:飞书日历的事件创建和腾讯会议的创建是异步的,如果处理不当,会出现日程已经创建但会议号还没回填的情况。
我们的解决方案是:在中间服务里维护一个任务队列。当监听到飞书日历事件创建后,先把任务放入队列,立即返回成功响应给飞书(避免飞书重试)。然后后台Worker从队列中取出任务,调用腾讯会议API创建会议,再调用飞书日历更新接口回填信息。如果腾讯会议API调用失败,任务会重试三次,三次都失败则发送告警消息到运维群。
# 任务队列的简化处理逻辑 from celery import Celery app = Celery('tasks', broker='redis://localhost:6379/0') @app.task(bind=True, max_retries=3) def create_meeting_and_update_calendar(self, event_data): try: # 调用腾讯会议API创建会议 meeting_info = tencent_meeting_client.create_meeting( subject=event_data['summary'][:40], start_time=event_data['start_time'] // 1000, end_time=event_data['end_time'] // 1000 ) # 回填到飞书日历 feishu_client.update_calendar_event( event_id=event_data['event_id'], description=f"会议号:{meeting_info['meeting_code']}\n" f"入会链接:{meeting_info['join_url']}\n" f"密码:{meeting_info['password']}" ) except Exception as exc: # 重试,采用指数退避策略 raise self.retry(exc=exc, countdown=2 ** self.request.retries)提示:飞书日历的更新接口有频率限制,建议在任务队列中做限流,避免短时间内大量更新触发限流。
4. Webhook事件订阅与会议状态流转处理
4.1 腾讯会议Webhook的签名验证与事件类型
腾讯会议的Webhook回调需要验证签名,签名算法是:把时间戳、随机字符串、请求体拼接后,用SecretKey做HMAC-SHA256加密,然后Base64编码。服务端收到回调后,用同样的算法计算签名,与请求头中的签名比对,一致才处理。
我们订阅的事件类型主要有四种:
- meeting.started:会议开始,用于在飞书群里发送"会议已开始"的提醒。
- meeting.ended:会议结束,触发录制文件处理流程。
- recording.completed:录制完成,获取录制文件下载地址。
- smart_summary.completed:智能纪要生成完成,获取纪要内容。
这里有一个坑:腾讯会议的Webhook回调有重试机制,如果服务端没有在5秒内返回200,腾讯会议会重试推送,最多重试3次。这意味着你的处理逻辑必须足够快,或者采用"先响应、后处理"的模式。我们的做法是:收到回调后,先验证签名,然后把事件放入消息队列,立即返回200,后台Worker再慢慢处理。
4.2 会议结束事件的触发条件与延迟处理
meeting.ended事件并不是在会议结束的瞬间触发的,而是有一个延迟。根据我们的实测,延迟时间在30秒到2分钟之间。这个延迟是正常的,因为腾讯会议需要确认所有参会人都已离开,并且完成一些清理工作。
但录制文件的生成时间就更长了。recording.completed事件通常在会议结束后5到15分钟才触发,具体取决于会议时长和录制文件大小。我们的处理策略是:收到meeting.ended事件后,先发送一条"会议已结束,录制文件正在生成中"的提示消息到飞书群。等收到recording.completed事件后,再发送包含录制链接的完整消息。
这里有一个细节需要注意:如果会议没有开启录制,就不会有recording.completed事件。所以我们的逻辑里加了一个判断:如果会议结束后30分钟内没有收到录制完成事件,就发送一条"本次会议未开启录制"的提示,避免用户一直等待。
4.3 事件幂等处理:避免重复推送消息
Webhook回调可能会重复推送同一个事件,尤其是在网络抖动或者服务端响应超时的情况下。如果不做幂等处理,飞书群里就会出现多条重复的会议纪要消息,体验很差。
我们的做法是:在Redis里维护一个事件ID的集合,每个事件处理前先检查是否已经处理过。如果已经处理过,直接返回200,不再执行后续逻辑。事件ID的过期时间设置为24小时,足够覆盖腾讯会议的重试周期。
# 幂等处理的简化逻辑 def handle_webhook_event(event): event_id = event['event_id'] # 使用Redis的SETNX实现原子性检查 if not redis.setnx(f"event:{event_id}", "1"): # 事件已处理过,直接返回 return {"code": 0} # 设置过期时间 redis.expire(f"event:{event_id}", 86400) # 处理事件 process_event(event) return {"code": 0}注意:幂等处理的key一定要包含事件ID,不要用会议ID,因为同一个会议会有多个事件(开始、结束、录制完成等)。
5. 录制文件与智能纪要的回传链路
5.1 录制文件下载与飞书云空间上传
腾讯会议的录制文件默认存储在腾讯云上,下载地址有时效性,通常是7天。我们的做法是:收到recording.completed事件后,立即调用腾讯会议的文件下载接口,把录制文件下载到本地临时目录,然后调用飞书云文档的上传接口,上传到飞书云空间。
这里有一个大文件上传的问题:如果会议录制文件超过100MB,直接上传可能会超时。飞书云文档的上传接口支持分片上传,需要先把文件切成4MB的分片,逐个上传,最后调用完成上传接口合并。
# 分片上传的简化逻辑 def upload_large_file(file_path, file_name): # 初始化分片上传 upload_id = feishu_client.init_chunk_upload(file_name) chunk_size = 4 * 1024 * 1024 # 4MB with open(file_path, 'rb') as f: chunk_index = 0 while True: chunk = f.read(chunk_size) if not chunk: break feishu_client.upload_chunk(upload_id, chunk_index, chunk) chunk_index += 1 # 完成上传 file_token = feishu_client.complete_chunk_upload(upload_id) return file_token上传完成后,飞书会返回一个file_token,需要再调用获取文件元信息接口,拿到文件的分享链接。这个链接可以直接在飞书消息卡片中展示,点击即可在飞书内预览或下载。
5.2 智能纪要的格式转换与消息卡片组装
腾讯会议的智能纪要返回的是结构化的JSON数据,包含会议主题、参会人、发言摘要、待办事项等字段。飞书的消息卡片支持Markdown格式,但字段结构需要自己组装。
我们设计的消息卡片包含四个部分:
- 会议基本信息:会议主题、开始时间、结束时间、参会人数。
- 录制文件链接:如果有录制,展示"查看录制"按钮。
- 智能纪要摘要:截取前200字的会议摘要,附上"查看完整纪要"按钮。
- 待办事项列表:把智能纪要中提取的待办事项,以复选框的形式展示。
这里有一个经验:智能纪要的待办事项提取准确率不是100%,有时候会把一些讨论内容误判为待办。我们的做法是在消息卡片底部加一个"编辑待办"的按钮,点击后跳转到飞书任务页面,用户可以手动调整。
5.3 待办事项自动生成飞书任务的实现
飞书任务(Task)的API支持创建任务、设置负责人、设置截止时间。我们把智能纪要中的待办事项提取出来后,调用飞书任务API批量创建任务。
这里有一个细节:待办事项的负责人识别。智能纪要中通常会提到"张三负责跟进",但格式不固定,有时候是"张三",有时候是"@张三",有时候是"张三同学"。我们的做法是用正则表达式匹配"@xxx"和"xxx负责"两种模式,匹配到的名字再去飞书通讯录里查找对应的用户ID。如果找不到,就把任务创建为无负责人状态,并在消息卡片中提示用户手动指派。
# 待办事项提取的简化逻辑 import re def extract_todos(summary_text): todos = [] # 匹配 @xxx 或 xxx负责 的模式 patterns = [ r'@(\w+)\s+(.+)', r'(\w+)负责(.+)' ] for pattern in patterns: matches = re.findall(pattern, summary_text) for match in matches: todos.append({ 'assignee': match[0], 'content': match[1].strip() }) return todos提示:飞书任务的创建接口有频率限制,建议批量创建时加一个100毫秒的间隔,避免触发限流。
6. 实操中踩过的坑与排查思路
6.1 Token过期导致的鉴权失败:一次完整的排查过程
上线后的第三天,运维群里突然收到告警:飞书消息推送失败,错误码是99991663,提示"tenant_access_token invalid"。我第一反应是Token过期了,但检查了Token管理器的日志,发现Token在30分钟前刚刚刷新过。
排查过程是这样的:先看Token管理器的日志,确认刷新成功;然后看API调用的日志,发现失败的那次调用用的Token和刷新后的Token不一致。问题定位到了:我们的服务部署了两个实例,实例A刷新了Token,但实例B还在用内存里缓存的旧Token。
解决方案就是把Token缓存从内存迁移到Redis,两个实例共享同一个Token。这个问题在单实例部署时不会出现,但一旦做了负载均衡就会暴露。如果你也打算做多实例部署,建议一开始就用共享缓存。
6.2 会议号回填失败:日历更新接口的并发限制
有一段时间,用户反馈说创建日程后,会议号有时候能回填,有时候不能。排查后发现是飞书日历更新接口的并发限制问题:同一个日历的更新操作,每秒最多5次。当多个用户同时创建日程时,更新请求会排队,超过限制的请求会被拒绝。
我们的解决方案是在任务队列中增加一个限流器,用Redis的令牌桶算法控制更新频率。具体来说,每个日历维护一个令牌桶,每秒生成5个令牌,更新请求需要先获取令牌才能执行。这个改动之后,会议号回填的成功率从85%提升到了99.9%。
6.3 录制文件下载超时:大文件分片与断点续传
前面提到过,大文件上传需要分片。但下载同样有问题:腾讯会议的录制文件下载接口,如果文件超过500MB,直接下载可能会超时。我们的做法是先用HEAD请求获取文件大小,如果超过100MB,就采用Range请求分片下载,每片10MB,下载完一片写入本地文件,最后合并。
# 分片下载的简化逻辑 def download_large_file(url, save_path): # 获取文件大小 head_resp = requests.head(url) file_size = int(head_resp.headers['Content-Length']) chunk_size = 10 * 1024 * 1024 # 10MB with open(save_path, 'wb') as f: for start in range(0, file_size, chunk_size): end = min(start + chunk_size - 1, file_size - 1) headers = {'Range': f'bytes={start}-{end}'} resp = requests.get(url, headers=headers) f.write(resp.content) return save_path这个方案还有一个好处:如果下载中途失败,可以从最后一个成功的分片继续下载,不用从头开始。
6.4 消息卡片按钮无响应:飞书卡片回调的配置陷阱
飞书消息卡片上的按钮,点击后需要配置回调地址。我们一开始把回调地址配置成了内网地址,导致点击按钮没有任何反应。排查后发现,飞书的卡片回调必须是一个公网可访问的HTTPS地址,而且需要在飞书开放平台的后台配置白名单。
另外,卡片回调的请求体格式和普通消息回调不一样,需要单独解析。我们的做法是写一个统一的回调处理器,根据请求体中的type字段判断是卡片回调还是消息回调,分别处理。
7. 这套对接方案还能怎么扩展
7.1 会议数据沉淀到飞书多维表格做统计分析
我们最近在做的一个扩展是:把每次会议的基本信息(主题、时长、参会人数、录制文件大小)写入飞书多维表格,然后利用多维表格的仪表盘功能做统计分析。比如,可以直观地看到哪个部门的会议最多、平均会议时长是多少、哪些会议没有开启录制等。
这个扩展的实现很简单:在会议结束事件的处理逻辑中,增加一步调用飞书多维表格的写入接口。需要注意的是,多维表格的字段类型要提前定义好,日期字段要传时间戳,人员字段要传用户ID。
7.2 与飞书审批流结合:会议室预定与会议创建的联动
另一个有意思的扩展方向是跟飞书审批流结合。比如,员工在飞书里提交一个"会议室预定"审批,审批通过后,自动在腾讯会议创建对应的线上会议,并把会议号回填到审批单里。这样线下会议室和线上会议就绑定在一起了,参会人既能到现场,也能远程接入。
这个扩展需要用到飞书审批的实例回调,在审批通过的事件中触发会议创建逻辑。审批流的字段映射比日历复杂一些,因为审批单的字段是自定义的,需要根据审批模板的ID做不同的处理。
7.3 常见问题速查表
最后整理一份我们运维过程中积累的常见问题速查表,方便遇到问题时快速定位:
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 消息推送失败,错误码99991663 | Token过期或无效 | 检查Token管理器日志,确认刷新是否成功 |
| 会议号未回填到日历 | 日历更新接口限流 | 检查任务队列的限流配置,查看是否有429错误 |
| 录制文件下载失败 | 下载地址过期或文件过大 | 检查事件处理延迟,确认是否在7天内下载 |
| 卡片按钮点击无反应 | 回调地址未配置或非公网 | 检查飞书开放平台后台的回调配置 |
| 待办事项负责人识别错误 | 智能纪要格式不固定 | 优化正则表达式,增加人工确认环节 |
这套对接方案从最初的简单消息推送到现在完整的会议生命周期管理,前后迭代了大概五六个版本。最大的体会是:不要试图一次性把所有功能都做完,先把最核心的"会议创建+录制回传"跑通,然后再逐步增加智能纪要、待办生成、数据分析这些扩展功能。每增加一个功能,都要确保有完善的错误处理和告警机制,否则出了问题很难排查。
另外,飞书和腾讯会议的API都在持续更新,建议定期关注官方文档的变更日志,避免因为接口调整导致服务不可用。我们现在的做法是每个月做一次回归测试,确保核心链路始终可用。