如果你所在团队同时用飞书聊日常、用腾讯会议开线上会,那你大概率遭遇过这个场景:会议前10分钟,群里还在陆陆续续有人问"链接呢?",发起人一边刷新腾讯会议后台,一边把会议号复制到群里,再手动@所有人。一次两次还能忍,每周三次就有点崩溃了。我接手这个"飞书-腾讯会议对接"的小项目,最初动机就是把这段手工链路彻底废掉:在飞书群里发一条指令,机器人自动去腾讯会议侧创建会议,把会议信息以消息卡片推回群里,会后再把纪要同步到飞书文档。这篇文章就是我整个对接过程的完整复盘。
先说结论:这条链路完全走得通,飞书开放平台和腾讯会议开放接口的文档都算清晰,真正耗时间的不是"调通接口",而是权限设计、回调验签、token缓存这类细节。下面我会按"为什么做、准备什么、怎么实现、踩了哪些坑、还能怎么扩展"的顺序,把每一段关键代码和配置逻辑摊开讲。
1. 双平台协作的痛点:为什么值得做对接
先量化一下手工模式的成本。我们团队每周固定周会2场、跨部门评审会2~3场,每场会议从创建到通知完毕的重复动作是:打开腾讯会议客户端 -> 点击预定会议 -> 填入主题、时间 -> 复制会议链接 -> 切到飞书群 -> 粘贴链接 -> 补充会议主题和议程。一次操作大约3分钟,5场就是15分钟,一个月就是1小时纯手工劳动。如果遇上会议改期,整套动作还要再走一遍。
这些操作还只是"成本可感知"的部分,更大的隐患是出错。手动复制链接很容易把"会议号"和"会议链接"搞混,或者忘记设置密码,参会人点进来才发现要等主持人批准。用API对接之后,这类低级事故基本绝迹。
所以这个项目的目标拆成三条:
- 在飞书群里用斜杠指令或消息触发,机器人自动创建腾讯会议
- 创建结果以卡片消息返回,包含会议主题、时间、链接、会议号
- 支持查询某天的会议列表,以及后续的参会状态回写
我评估过三条路线:
| 方案 | 维护成本 | 体验 | 适用规模 |
|---|---|---|---|
| 纯手动复制链接 | 零 | 差,易出错 | 每周少于2场 |
| 半自动:客户端预定 + 飞书机器人手动推送链接 | 低 | 一般 | 临时接入、验证阶段 |
| 全自动:服务端调API创建会议 + 卡片回传 | 中 | 好 | 每周3场以上,值得投入 |
我们最终选择全自动,因为腾讯会议开放API的接口文档成熟度不错,飞书机器人卡片生态也友好,整条链路没有明显的技术断点。如果你只是每周1场周会,且不需要会议数据沉淀,我建议不要碰API,手动能解决的问题不值得多维护一个服务。
2. 对接前的基础配置:飞书应用与腾讯会议API的准备工作
很多新手第一次做这类对接,上手就写代码,结果卡在"权限点找不到""密钥不知道在哪申请"上。基础配置值得单独拿出来说,因为这里理解的偏差会导致后面反复返工。
2.1 飞书侧:自建应用的权限范围不可小看
需要到飞书开放平台创建企业自建应用。创建完成后的关键不是拿到App ID和App Secret就完事,而是要仔细梳理应用需要的能力与权限。
我这个项目用到了三块能力:
- 机器人能力:用于接收群内消息,以及向群会话发送消息卡片
- 事件订阅:用于接收消息事件(
im.message.receive_v1),这是机器人感知用户指令的入口 - 云文档能力:用于后期会议纪要自动写入飞书文档
对应要申请的权限点主要有以下几个,申请时建议按最小授权原则来:
| 权限点 | 作用 |
|---|---|
im:message | 读取与发送单聊/群聊消息 |
im:message:send_as_bot | 以机器人身份发送消息 |
im:chat:readonly | 读取群基础信息,比如拿群名称 |
docx:document | 创建与编辑飞书文档(纪要回写用) |
这里最容易踩的一个坑是:在飞书开放平台后台,你需要在"权限管理"页面手动开通这些权限点,并且发布应用版本后权限才生效。版本发布还要经过管理员审核,如果你们企业管理员审核比较严格,一定要提前把权限用途写清楚,我第一次申请就被打回过一次,理由写的是"需要说明为何要读取全部消息",后来改成"仅接收群内@机器人的消息事件"就通过了。
还有一个细节:飞书API访问身份分两种,tenant_access_token(应用身份)和user_access_token(用户身份)。创建会议这种机器人主动行为用tenant_access_token足够,如果涉及读写某个用户的个人日历,那就必须走OAuth拿user_access_token。我们这个方案里不需要读取个人日历,所以全程用应用身份,大大简化了流程。
2.2 腾讯会议侧:应用ID与密钥的获取流程
腾讯会议开放平台的对接需要先完成企业认证,然后用企业管理员账号登录腾讯会议开放平台,创建一个"会议应用",拿到以下信息:
- App ID:应用的唯一标识
- App Secret:用于签名请求
- 企业ID(如果调用管理端接口会用到)
注意这里的Secret不是腾讯云API密钥,而是腾讯会议开放平台的应用密钥,两者经常被混为一谈。做接口签名时用的是后者。
拿到密钥后,强烈建议先在腾讯会议开放平台的"API调试工具"里把创建会议接口调通一遍。调试工具里能直接看到签名前后的完整请求,你可以用这个来对照自己的代码签名是否正确。我第一次就是在这里发现时间戳参数必须用Unix秒,而我在代码里传成了毫秒,导致请求一直返回签名错误。
2.3 内外网联调的关键:回调地址与白名单
对接过程中,飞书和腾讯会议都会要求配置回调URL。飞书这边是事件订阅的请求地址,腾讯会议这边是会议事件通知的接收地址。两者都要求你的服务必须能被公网访问,且域名需要配置HTTPS证书。
如果你在公司内网开发,又没有现成的公网服务器,我建议第一阶段先用frp或内网穿透工具把本地服务暴露出去,但只用于开发联调,上线前一定要切到正式服务器。我踩过的一个坑是:飞书事件订阅的URL验证是GET请求带challenge参数,当时我本地服务没把公网请求转发到正确的端口,导致验证一直失败,白白耗了半小时查代码。
另外,飞书的回调地址支持在应用后台配置"回调路由",腾讯会议的Webhook虽然也支持配置,但回调接口需要拦截并验签。这部分逻辑我会在下一节详细展开。
3. 一整套对接链路:从飞书指令到腾讯会议创建再到卡片回传
核心链路如果用一句话概括就是:用户在群里@机器人并发消息 -> 飞书把事件回调到我们的服务 -> 服务解析指令并调用腾讯会议API -> 拿到会议链接后调用飞书API发送卡片消息。
我把整个服务拆成了三个模块:消息接收与解析、会议创建、卡片回传。三者之间通过内部函数调用串联,不引入消息队列,因为请求量很小,没必要把架构搞复杂。
3.1 飞书消息事件的处理与验签逻辑
先看消息事件的处理。飞书会在用户@机器人发消息时,把事件POST到你在后台配置的请求地址,最关键的两个要求:
- 处理URL验证:飞书首次配置回调地址时,会发送一个GET请求,参数包含
challenge和token,服务端返回challenge字段的值即完成验证 - 处理事件推送:后续消息事件是POST请求,header里带
X-Lark-Request-Timestamp和X-Lark-Signature,需要做签名校验
签名校验逻辑很简单:把请求时间戳、请求体、Encrypt Key拼接后做HMAC-SHA256,再与签名对比。这个校验不能省,否则任何人都可以伪造消息触发你的服务。
import hashlib import hmac import json from flask import Flask, request, jsonify app = Flask(__name__) APP_SECRET = "your_feishu_app_secret" ENCRYPT_KEY = "your_encrypt_key" def verify_feishu_signature(timestamp, nonce, body, signature): string_to_sign = f"{timestamp}{nonce}{ENCRYPT_KEY}" h = hmac.new(string_to_sign.encode("utf-8"), body.encode("utf-8"), hashlib.sha256) return h.hexdigest() == signature @app.route("/feishu/callback", methods=["GET", "POST"]) def feishu_callback(): if request.method == "GET": # URL验证 return jsonify({"challenge": request.args.get("challenge")}) # 事件推送 timestamp = request.headers.get("X-Lark-Request-Timestamp") nonce = request.headers.get("X-Lark-Request-Nonce") signature = request.headers.get("X-Lark-Signature") body = request.get_data(as_text=True) if not verify_feishu_signature(timestamp, nonce, body, signature): return jsonify({"code": 1, "msg": "signature error"}), 403 event = json.loads(body) # 具体事件处理 handle_feishu_event(event) return jsonify({"code": 0, "msg": "success"})消息解析时,重点关注event.message.content字段,它是一段JSON字符串,text字段是用户实际发送的文本。由于飞书会把@机器人的内容也放进text里,所以需要先去掉@部分再提取指令。
3.2 创建腾讯会议:签名、参数与时间处理
腾讯会议API的签名机制用的是HMAC-SHA256,比飞书稍微绕一点。它需要把App ID、Secret、请求方法、请求路径、请求时间和随机字符串拼成一个规范字符串,然后做摘要。具体规则文档里写得很全,但有几个容易忽略的细节:
X-TC-Key是App IDX-TC-Timestamp是Unix秒级时间戳X-TC-Nonce是随机字符串,每次请求都要变- 签名时对请求体也会参与计算,JSON的序列化顺序会影响结果
下面是我整理出来的创建会议请求伪代码,核心是把请求体和签名参数准备好:
import time import json import hashlib import hmac import requests APP_ID = "your_tencent_meeting_app_id" APP_SECRET = "your_tencent_meeting_app_secret" def generate_signature(method, path, params, timestamp, nonce): # 拼接规范字符串,注意顺序不要弄错 query_string = "" # 这里需要根据腾讯会议签名文档将params转成规范的query string string_to_sign = f"{method}\n{path}\n{query_string}\n{timestamp}\n{nonce}" h = hmac.new(APP_SECRET.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256) return h.hexdigest() def create_tencent_meeting(subject, start_time, end_time, meeting_type=0): timestamp = int(time.time()) nonce = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16)) payload = { "meeting_id": "", # 传空表示创建新会议 "meeting_info_list": [{ "subject": subject, "type": meeting_type, # 0为预约会议 "start_time": str(start_time), "end_time": str(end_time), "settings": { "mute_enable": 0, # 不默认全员静音 "allow_enter_watermark": True } }] } headers = { "X-TC-Key": APP_ID, "X-TC-Timestamp": str(timestamp), "X-TC-Nonce": nonce, "X-TC-Signature": generate_signature("POST", "/v1/meetings", "", timestamp, nonce), "Content-Type": "application/json" } resp = requests.post( "https://api.meeting.qq.com/v1/meetings", json=payload, headers=headers ) return resp.json()创建成功之后,返回体里会带meeting_info_list[0].meeting_code(会议号)和meeting_info_list[0].join_url(入会链接),这两个值就是后面卡片消息要用的核心数据。
3.3 把会议信息以消息卡片推回飞书群
拿到会议号和链接后,推送消息我推荐用飞书的"消息卡片"而非纯文本。卡片的好处是结构清晰,参会人一眼就能看到主题和时间,而且可以配置按钮,比如"点击入会"直接跳转。
消息卡片的JSON结构可以自己拼,也可以用飞书后台的"可视化编辑器"生成后贴在代码里。我习惯用interactive类型的卡片,灵活度高。核心代码就三件事:组装卡片JSON、获取tenant_access_token、调用消息发送API。
def send_feishu_card(chat_id, subject, start_time, meeting_code, join_url): token = get_tenant_access_token() card_content = { "config": {"wide_screen_mode": True}, "header": { "title": {"tag": "plain_text", "content": f"会议通知:{subject}"}, "template": "blue" }, "elements": [ {"tag": "div", "text": {"tag": "lark_md", "content": f"**开始时间**: {start_time}"}}, {"tag": "div", "text": {"tag": "lark_md", "content": f"**会议号**: {meeting_code}"}}, {"tag": "div", "text": {"tag": "lark_md", "content": f"**入会链接**: [点击入会]({join_url})"}} ] } payload = { "receive_id": chat_id, "msg_type": "interactive", "content": json.dumps(card_content) } headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } resp = requests.post( f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id", json=payload, headers=headers ) return resp.json()发送时receive_id_type要跟receive_id的类型匹配。如果receive_id传的是群ID,就用chat_id;如果传的是用户ID,就用open_id。我当时在这里差点搞混,导致消息发不出去,返回receive_id_type invalid。
4. 踩坑记录:授权失败、时间跳变和回调验签
整条链路代码写起来不过几百行,真正折磨人的是排错阶段。我按时间顺序记录了几个印象最深的坑,每一个都至少让我多花了一个小时。
4.1 tenant_access_token的缓存与过期
飞书的tenant_access_token有效期为2小时,但官方接口在过期后调用会返回错误码99991663(token expired)。问题是这个错误信息有延迟——你第一次拿到token后,即使还没到2小时,也可能因为飞书侧滚动刷新而导致旧的token立即失效。
我的教训是:不要每次请求都去申请新token,也不要只用缓存不刷新。正确的做法是本地缓存token,并记录拿到token时的系统时间,过期前5分钟主动刷新。如果遇到99991663错误,就清掉缓存重新获取一次,最多重试一次。
_token_cache = {"token": None, "expire_at": 0} def get_tenant_access_token(): now = time.time() if _token_cache["token"] and now < _token_cache["expire_at"] - 300: return _token_cache["token"] resp = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", json={ "app_id": APP_ID, "app_secret": APP_SECRET }) data = resp.json() _token_cache["token"] = data["tenant_access_token"] _token_cache["expire_at"] = now + data["expire"] + 60 return _token_cache["token"]4.2 腾讯会议创建接口的"时间必须为未来时间"
有段时间,我测试时总是把start_time设为当前时间的前几分钟,因为我想立刻看到结果。结果腾讯会议API直接返回错误,提示start_time must be in the future。这个限制很合理,因为预约会议本质上要求开始时间晚于当前时间。
但问题是,如果用户发指令到会议开始的时间间隔太短,比如提前1分钟才发指令,也会撞上这个限制。我在产品逻辑上做了一个兜底:如果用户指定的start_time与当前时间差小于5分钟,就自动把start_time改为当前时间+5分钟,并在返回卡片里提醒用户会议时间已被微调。这不算什么高深技术,但用户感知很好,至少不会莫名其妙创建失败。
4.3 飞书回调验签:Encrypt Key不是App Secret
前面提到回调验签时需要Encrypt Key,这个值在飞书开放平台应用的"事件与回调"配置页里,和App Secret是两个不同的东西。而且如果你的应用开启了"加密"选项,回调请求体本身还是加密的,需要先用Encrypt Key解密出明文JSON再处理。
我踩这个坑的时候,验签一直通过,但解析出来的event对象是空的,后来发现是因为没有处理encrypt字段。如果你的回调体长这样:
{"encrypt": "xxxxxxxxxxxx"}那么要先解密,飞书官方文档给出的方案是先用AES-256-GCM解密encrypt字段,得到JSON格式的原始事件体,再做后续处理。这一点在调试时特别容易被忽略,因为接口返回的HTTP状态码是200,但业务逻辑走不通。
4.4 腾讯会议API签名参数排序的隐形规则
在对接腾讯会议API时,最隐蔽的问题是签名串中的参数必须按照参数名的ASCII码升序排列。比如meeting_id和meeting_info_list这两个参数,meeting_id要排前面,因为d小于i。如果不排序,签名校验会失败。
这种问题最坑人的地方在于:你在官方调试工具里把请求参数复制过来,肉眼看起来是对的,但两个系统的签名结果就是不一致。我最后的排查方案是写了一个脚本,把待签名字符串打印出来,逐字节对比官方调试工具里的字符串,这才发现多了一个&字符。如果你也遇到签名错误,我建议你第一件事就是打开"打印待签名字符串"的日志,而不是怀疑加密算法写错了。
5. 进阶扩展:会议纪要回写、参会状态同步与机器人告警
当"在飞书里创建腾讯会议"这条路跑通之后,你会发现可以做的事情突然变多了。因为飞书和腾讯会议两个平台的API能力都开放出来了,剩下的是业务想象力。我在这套基建上陆续加了三个功能,都收到了不错的效果。
5.1 会议纪要通过机器人写入飞书文档
每次会议结束后,腾讯会议默认会生成一份录制文件和转写文本,但这些内容没有自动回到飞书生态里。我的做法是:订阅腾讯会议的"会议结束"事件,拿到会议的meeting_id,调用腾讯会议录制接口获取转写文件URL,然后把文本内容通过飞书云文档API写入一篇新的飞书文档,文档链接再推送到会议室群。
这里有一个必须提的点:腾讯会议的转写文件是音频转写,格式是带开始时间和说话人标记的文本,直接丢进飞书文档会显得很乱。我做了一层清洗:把发言人声明保留,去掉重复的时间戳行,再按照段落长度做二次分段。不需要做AI总结,纯文本整理就足够让团队成员快速跳转定位。
5.2 参会状态同步到飞书多维表格
腾讯会议企微版或开放平台提供的会议明细接口,能拿到某次会议的参会人列表、入会时间、离会时间。我把这些数据定时同步到飞书多维表格里,每行一个参会人,字段包括姓名、部门、入会时间、参会时长。这样运营团队月底复盘会议出勤率时,不再需要人工翻腾讯会议后台。
同步的定时触发我用的是飞书自带的定时卡片能力,让用户可以设定同步频率,到点后由机器人自动执行。这里要提醒一个问题:腾讯会议开放API对单次查询的时间跨度有限制,超过一定天数会返回参数错误,所以我做同步时按天分批拉取,再合并写入多维表格。
5.3 异常状态告警:会议超时、录制失败
大家都有过这种经历:会议预定2小时,结果1.5小时就结束了,但腾讯会议后台还挂着录制任务。为了防止这种情况,我在回调逻辑里加了异常监控:会议结束时间超过预定结束时间15分钟,机器人就在告警群里推送提醒,让管理员确认是否需要延长会议。
这个功能听起来简单,但做的时候需要注意一点:回调事件里带的meeting_id和创建时返回的meeting_id是否一致。腾讯会议的接口里存在两类ID,一个是meeting_id(用于API查询),一个是meeting_code(用于入会的数字会议号),回调事件里给的是meeting_id。拿meeting_code去查询下次会议状态就会查不到,这个映射关系最好在一开始的数据库设计里就建立好。
6. 一些实话:什么情况不建议用API对接
写到这里,全是"技术能解决问题"的一面,但作为经历完整的对接开发者,我得说几句可能很多人不爱听的大实话。API不是银弹,有些团队根本不应该做这个对接。
- 会议频次低:如果你每周不超过2次会议,手动操作的成本远低于开发、部署、维护这套系统的成本。别为了技术成就感去做需求不存在的项目。
- 使用场景局限在固定周会:固定周会的会议链接完全可以提前一周创建好并置顶在群公告里。没必要动态创建。
- 没有可维护的服务器与域名:这套系统离不开一台常驻服务器和HTTPS域名。如果谁临时起意用自己电脑跑,一旦关机,飞书机器人就"死"了,参会体验反而变差。
如果上述条件都不满足,那我的建议是:先跑通最小闭环,再考虑扩展。最小闭环就是机器人创建会议 + 卡片回传,不要一上来就做纪要、多维表格、告警这些花活。等用户真的用得频繁了,再按需求优先级一个一个加。
这套系统在我们这边稳定运行了几个月,最大的变化不是"省了多少分钟",而是团队对"机器人发起会议"这个动作产生了信任。大家已经默认群里那个"会议通知"卡片是唯一权威来源,不会再有人问链接在哪。对我个人而言,这段对接经历最值钱的部分不是写了多少代码,而是把两个完全不同平台的鉴权体系、事件模型和接口约束摸了一遍,以后再遇到类似"平台A对接平台B"的需求,我心里就有了一张清晰的排查地图。