
我们团队的工作沟通基本都在飞书里但客户会议、外部交流会又习惯用腾讯会议。每天最烦的一个动作就是打开腾讯会议客户端新建会议复制会议号切回飞书找到群粘贴链接再手动参会人。重复几周之后我决定把这两个系统打通。这篇文章就是一次飞书和腾讯会议对接的实践记录从权限准备、接口调用到踩坑修复完整过了一遍。无论你是开发、运维还是IT管理员只要团队里同时存在飞书和腾讯会议这两套工具这次实践里遇到的问题和对应解法大概率你也会遇到。1. 先理清需求边界飞书和腾讯会议之间到底能对接什么很多人在动手前容易犯一个错误一上来就问“能不能把飞书和腾讯会议打通”。能但“打通”这两个字太宽泛。你需要先想清楚你要的到底是“自动把会议链接发进群”还是“飞书日历上点一下就能创建一个腾讯会议”还是“会议结束之后自动把录制文件同步回来”。需求不一样技术方案完全是两码事。1.1 先从使用场景反推你的团队到底想要什么我总结了一下团队里的真实需求大概可以归成下面几类创建会议后自动把腾讯会议号、入会链接、会议时间发到飞书群或个人。在飞书日历里创建日程时自动关联一个腾讯会议。在飞书群里机器人说“下午3点开一个和XX客户的会”机器人自动创建腾讯会议并返回入会信息。会议结束后通过飞书机器人发送会议纪要或邀请大家查看录制文件。管理员用飞书机器人查询正在进行的腾讯会议甚至强制结束某个会议。这些场景不是都难但也不是都简单。相对容易的是“飞书机器人调腾讯会议API创建会议然后把链接发回飞书”这是最基础的闭环。后面做的日历联动、卡片回调、状态同步都是在基础闭环上长出来的。1.2 两份API文档透露的能力边界飞书开放平台提供的是“自建应用”模式你创建一个应用后可以开机器人、云文档、日历、事件订阅这些能力。每个能力对应一组权限比如发消息要开im:message:send_as_bot读写云文档要开docx:document读写日历要开calendar:calendar。腾讯会议开放平台则是另一套逻辑。首先要明确一点个人免费版腾讯会议账号几乎调不了开放接口。你必须有企业账号并且管理员要在腾讯会议开放平台创建应用。创建应用后你会拿到App ID、Secret ID、Secret Key或者走OAuth授权拿到用户身份凭证。大部分“程序自动创建会议”的需求用JWT鉴权就够了。另外腾讯会议开放平台的API没有沙箱环境调试时调用的是真实会议服务所以测试时要小心不然你会一秒创建出十多个会议通知把同事的飞书群炸了。1.3 我在项目里锁定的数据流对接这类系统最怕边界不清。我在动手前在文档里画了一条数据流后面所有代码都围绕这条数据流展开触发源产生可能是飞书群里收到一条机器人的消息也可能是飞书日历里新建了一个日程事件。飞书事件订阅把消息或事件推送给后端的回调接口。后端验证请求签名解析出会议主题、开始时间、参会人等信息。后端调用腾讯会议API创建会议拿到会议号、入会链接和会议ID。后端调用飞书API把会议信息发给对应群或用户。当会议状态变化时比如有人入会、会议结束腾讯会议回调通知后端后端再更新飞书侧的状态或发送提醒。这条链路看起来很长但可以拆成几段逐步落地。第一步先做第4和第5步也就是最小闭环。跑通后再把第1到3步和事件订阅接进来。如果一开始就想着把全流程一起做完大概率会因为调试困难而放弃。2. 应用创建与权限准备两边各要填怎样的“坑”这个阶段最容易让人栽跟头。我见过不少同事卡在“开了权限但还是调用失败”这个环节。原因是权限申请列表很长漏掉一个不起眼的权限前端报错根本看不出来。2.1 飞书自建应用不是建完就能发消息飞书开放平台创建应用后第一件事是把“机器人”能力打开否则你的应用只是一个空壳不能出现在群里。接着去“权限管理”里开权限。常用权限包括im:message:send_as_bot以机器人身份发消息。im:message:readonly读取群消息这是接收机器人消息的前提。im:chat:readonly读取群基础信息比如群名称和群成员。calendar:calendar读写日历事件日历联动会用到。docx:document读写云文档后面做会议纪要推送会用到。开完权限后一定要“创建版本并发布”然后等管理员审批。很多第一次接触的人以为开完权限就生效了结果调用接口一直报权限不足其实是因为没发布版本。发布之后你会在“凭证与基础信息”里看到App ID和App Secret。这两个东西后面调用接口都要用。App Secret要严格保密别提交到代码仓库。2.2 腾讯会议开放平台个人账号真的不行腾讯会议开放平台和应用商店是两个入口这里说的是“开放平台”。管理员登录后在“应用管理”里创建一个应用类型选择“API接入”。创建完应用你会拿到一个App ID和一组Secret ID/Secret Key。为什么要两组因为腾讯会议API的JWT鉴权需要同时使用Secret ID和Secret KeySecret ID用来标识身份Secret Key用来对JWT签名。还需要配置IP白名单。腾讯会议API默认只允许来自白名单IP的请求调用如果你在本地调试要先把出口IP加进去。否则你构造的JWT完全正确也会一直提示鉴权失败。有一个更隐蔽的点创建腾讯会议API应用时如果你用的是会议企业管理员账号回调地址和IP白名单可以直接在控制台配置。但如果企业是由服务商代维的很多配置可能被服务商锁死你只能申请调整。这个在项目开始前就要确认否则开发完了没有生产环境的应用权限一切白搭。2.3 权限申请清单最容易漏掉的几项我把当时申请权限时踩过漏过的地方整理成一张表平台容易漏掉的权限/配置影响飞书im:message只开了单聊没开群聊机器人无法往群里发消息飞书事件订阅回调地址没填收不到消息无法事件驱动飞书云文档权限未单独授权向云文档写入纪要时报“无权限”腾讯会议未配置IP白名单请求一直返回鉴权失败腾讯会议未开通“历史会议”相关权限查不到已结束会议的录制文件腾讯会议回调地址未配置JWT验签参数回调可能被第三方拦截或伪造尤其是“飞书云文档授权”这个事我一开始以为有了tenant_access_token就能随便创建文档后来发现完全不是这样。应用身份写云文档需要额外授权用户身份的文档操作还要走OAuth。白白耽误了小半天所以我把这个坑单独放在后面的踩坑章节里讲。3. 最小闭环用Python创建腾讯会议再由飞书机器人推送先说结论最小闭环不需要事件订阅不需要数据库甚至不需要搭建服务写一个脚本就能跑通。但它是后面所有功能的地基。这一段的实现语言我用Python原因是腾讯会议SDK虽然提供官方SDK但直接用requests调REST接口反而更直观调试起来也方便。3.1 生成腾讯会议API所需的JWT Token腾讯会议API的JWT不是用来代替登录态的而是用来证明“调用方是一个合法的应用”。JWT的Payload里需要带上App ID、Secret ID、签发时间和过期时间。import time import jwt def generate_jwt(app_id: str, secret_id: str, secret_key: str) - str: iat int(time.time()) exp iat 1800 # 建议30分钟过期 payload { app_id: app_id, iat: iat, exp: exp, secret_id: secret_id, } return jwt.encode(payload, secret_key, algorithmHS256)exp不能设太长腾讯会议API对JWT的过期时间有校验30分钟比较安全。如果提前生成一个永久Token放着调用时会直接被拒绝。3.2 调用创建会议接口拿会议号和入会链接创建会议的接口是POST https://api.meeting.qq.com/v1/meetings。请求头里除了JWT还要带上Secret ID。import requests import json def create_tencent_meeting(subject: str, start_time: str, end_time: str): jwt_token generate_jwt(APP_ID, SECRET_ID, SECRET_KEY) headers { X-TC-Key: SECRET_ID, X-TC-Token: jwt_token, Content-Type: application/json, } body { subject: subject, start_time: start_time, end_time: end_time, meeting_type: 0, settings: { mute_enable_join: True, allow_unmute_self: False, }, } # 这里建议加external_id后面讲判重时会用到 resp requests.post(url, headersheaders, jsonbody) resp.raise_for_status() data resp.json() return data返回的data结构里核心字段在data[meeting_info]中包括meeting_id腾讯会议内部的会议唯一标识。meeting_code就是我们熟悉的那串数字会议号。join_url参会人入会链接这个要发到飞书群里。subject会议主题。需要注意腾讯会议API对时间的处理方式是东八区本地时间字符串格式类似2025-01-06 14:00。这个字符串不是时间戳使用时非常容易出时区问题具体在第5章里细说。3.3 飞书机器人发消息从换取tenant_access_token到发送成功飞书机器人发消息前先要用App ID和App Secret换取一个tenant_access_token。def get_tenant_access_token(app_id: str, app_secret: str) - str: url https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal resp requests.post(url, json{app_id: app_id, app_secret: app_secret}) resp.raise_for_status() return resp.json()[tenant_access_token]换到Token后调用飞书消息接口def send_feishu_message(token: str, chat_id: str, text: str): url https://open.feishu.cn/open-apis/im/v1/messages headers { Authorization: fBearer {token}, Content-Type: application/json, } body { receive_id: chat_id, msg_type: text, content: json.dumps({text: text}), } resp requests.post(url, params{receive_id_type: chat_id}, headersheaders, jsonbody) resp.raise_for_status()这里的chat_id怎么拿最简单的办法是把机器人拉进一个测试群然后让机器人主动接收群里的消息事件从事件回调里解析出chat_id。或者调用飞书的“获取群列表”接口从机器人所在群列表里找到目标群。3.4 判重逻辑避免同一个需求创建两场会最小闭环跑通后第二个问题马上来了脚本重复执行就会创建重复会议同事会收到一堆垃圾会议通知。腾讯会议API支持external_id字段你可以把业务方的唯一ID传进去。比如飞书消息的message_id这样即使同一事件被重复提交腾讯会议也不会重复创建会议而是返回已有的会议信息。body[external_id] feishu_msg_ message_id在代码里加一个缓存判断也很重要。我当时用Redis做了一层以external_id作为key命中就直接返回旧会议数据不重新调接口。这样既省了腾讯会议API的调用次数也避免了重复通知。4. 事件驱动进阶飞书群里机器人就能开会最小闭环的机器人只能被动发消息体验还差得很远。所以我们很快进入了第二阶段让群里的同事直接机器人“创建会议”机器人自动处理。这个阶段的核心是飞书的事件订阅。4.1 配置飞书事件订阅与回调地址在飞书开放平台的后台选择“事件与回调”添加事件im.message.receive_v1表示接收消息事件。注意要选择“消息被”的条件否则机器人会被群里的所有消息刷屏。回调地址需要是公网可访问的HTTPS地址。如果你只是在公司内网调试可以用内网穿透工具把本地服务暴露出去比如https://yourdomain.com/feishu/event。不要觉得这一步麻烦真正开发时几乎离不开回调调试。用FastAPI写一个最简单的回调服务from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app FastAPI() app.post(/feishu/event) async def feishu_event(request: Request): body await request.json() if body.get(type) url_verification: return JSONResponse({challenge: body[challenge]}) # 其他事件处理 return JSONResponse({code: 0})飞书首次配置回调地址时会发送一个url_verification请求你必须原样返回challenge字段否则验证不通过。这个很多人会漏掉。4.2 核心处理逻辑消息转会议收到群消息事件后事件体里会带message_id、chat_id、content等字段。content是JSON字符串里面包含消息文本。处理流程是这样的判断消息文本里有没有“创建会议”或类似触发词。从文本里解析会议主题和时间如果解析不了可以默认当前时间1小时后的会议。调用腾讯会议API创建会议。把会议链接通过飞书API发回当前群聊。import json import re def handle_message_event(body): event body[event] message_id event[message][message_id] chat_id event[message][chat_id] content json.loads(event[message][content]) text content.get(text, ) if 创建会议 not in text: return subject_match re.search(r主题[:](.), text) subject subject_match.group(1) if subject_match else 临时会议 meeting_data create_tencent_meeting(subject, 2025-01-06 14:00, 2025-01-06 15:00) meeting_info meeting_data[meeting_info] msg ( f会议已创建{subject}\n f会议号{meeting_info[meeting_code]}\n f入会链接{meeting_info[join_url]} ) token get_tenant_access_token(FEISHU_APP_ID, FEISHU_APP_SECRET) send_feishu_message(token, chat_id, msg)这里的时间解析可以用正则或者自然语言解析库。团队里如果只用固定模板话术正则就够了不用引入太重的东西。4.3 卡片消息与按钮回调纯文本消息能跑通但体验一般。后来我把推送消息改成了飞书卡片卡片上可以放“加入会议”“取消会议”两个按钮甚至展示当前会议状态。卡片按钮回调和普通事件订阅类似在飞书开放平台订阅card.action.trigger事件。用户点按钮后飞书会POST一个回调到同一个回调地址事件体里会带着action.value。比如“取消会议”按钮我在按钮value里塞了一个meeting_id{ action: cancel_meeting, meeting_id: 123456789 }后端收到后调用腾讯会议的“取消会议”接口再把卡片更新成“已取消”状态。这种交互方式比纯文本命令直观得多同事使用意愿也高了很多。不过要注意卡片按钮回调必须尽快返回超过3秒飞书会超时重试。所以不要在回调里直接执行耗时操作最好把任务丢到队列里异步处理。4.4 状态同步取消会议、修改时间、会议结束做到这一步我们发现只是“创建会议并通知”还不够。会议被取消、时间被修改飞书群里还留着一条过期信息很容易造成混乱。腾讯会议API提供了“查询会议详情”“修改会议”和“取消会议”的接口。如果在飞书侧维护一个meeting_id和message_id的关联关系就能实现用户点卡片“取消会议”后端取消腾讯会议同时更新卡片内容为“已取消”。用户重新调整会议时间后端调用修改会议接口再更新飞书消息。腾讯会议主动推送“会议结束”事件时后端在飞书群里发一条“会议已结束点击查看纪要”。状态同步看似美好但非常考验幂等设计。腾讯会议的会议更新接口支持meeting_id作为定位键但如果你在飞书事件回调里重复处理同一条更新指令还是会出乱子。所以我在数据库里维护了一张映射表字段大致是event_id飞书消息事件ID或日历事件ID。meeting_id腾讯会议ID。meeting_code会议号。feishu_message_id已经发送出去的飞书消息ID用来后续更新卡片。这样每次处理回调时先查这张表如果存在就直接返回旧数据不再触发腾讯会议API。5. 实战踩坑授权凭证、8小时时差、重复回调与限流做集成项目一半时间都在处理细节。下面这几个坑是我觉得最有代表性的如果你也做飞书和腾讯会议对接大概率会遇到。5.1 飞书云文档授权凭证那点事项目做到后面我想把会议纪要自动写入飞书云文档然后把文档链接发到群里。这里就踩了一个不小的坑。飞书云文档的访问权限分“应用身份”和“用户身份”。大多数情况下应用创建文档用的是用户授权而不是应用自己的tenant_access_token。你需要在飞书开放平台配置OAuth重定向URI然后让管理员或用户点击授权链接拿到一个code再用code换取user_access_token。很多团队的授权流程卡在“code获取不到”或者“invalid code”。常见原因有三个重定向URI和开放平台配置的地址不一致哪怕差一个斜杠都会失败。用户没有在应用权限里被分配“云文档”相关权限。user_access_token有效期短通常2小时左右过期后需要刷新。没有做刷新逻辑第二天写文档就报401。如果你用的是Dify这类低代码平台接飞书云文档第一次配置授权凭证时也要走同样的OAuth流程。只是这时候回调地址要填Dify提供的回调地址不是你自己写的后端地址。很多人把两个地方搞混导致凭证一直拿不到。5.2 会议时间为什么会差8小时我第一次调用腾讯会议API创建会议时明明在请求里写的是“下午2点”但同事看到飞书消息里显示的是“晚上10点”。查了半小时原因就是时区。腾讯会议创建会议接口虽然接受字符串时间但服务器在处理时会按你请求里的时区或默认时区转成时间戳。如果你本地系统时区是UTC或者代码里直接把浏览器传来的时间字符串传进去就会出现8小时偏差。解决办法是在应用层统一把时间转成东八区字符串或者统一使用Unix时间戳。from datetime import datetime from zoneinfo import ZoneInfo def to_beijing_time(dt_str: str) - str: dt datetime.fromisoformat(dt_str) dt_beijing dt.astimezone(ZoneInfo(Asia/Shanghai)) return dt_beijing.strftime(%Y-%m-%d %H:%M)在传给腾讯会议之前强制指定Asia/Shanghai。这样不管用户来自哪个时区会议时间都不会乱。5.3 回调重复并不可怕可怕的是没有幂等飞书事件订阅和腾讯会议回调都是“至少一次”投递也就是说同一个事件可能会被推送两三次。如果你直接把每次回调都当成新事件处理就会重复创建会议、重复发消息。解决手段有两个第一本地维护一张“已处理事件表”事件ID作为唯一键。处理前先查表没有记录才继续往下走。第二尽量复用腾讯会议的external_id字段。这个字段一旦传入腾讯会议会对相同external_id的请求做去重返回同一个会议信息。我把飞书消息的message_id塞到external_id里即使飞书重复推送腾讯会议也不会真的创建多场会。还有一个细节回调处理函数要保证“先返回后处理”。也就是说收到飞书回调后先把事件ID记录到Redis或数据库立刻返回200然后再异步去调用腾讯会议API。这样飞书不会认为你的服务超时而重试也不会把重试流量压到上游接口上。5.4 腾讯会议API限流429之后的处理经验腾讯会议开放平台的接口有QPS限制具体数值会随企业资质和实际用量浮动。但我遇到的最实际的场景是上午10点半几十个人同时点“创建会议”按钮后端一下收到上百个请求腾讯会议API开始大量返回429。我当时没有做限流代码里也没有重试机制结果就是部分同事收到会议创建成功部分人一直转圈。后来我做了三件事对飞书侧入口加锁同一个群或同一个用户同一秒内只能触发一次创建会议。对腾讯会议API调用加Token Bucket限流比如每分钟最多60次。遇到429或5xx时指数退避重试最多重试3次。import time import random def request_with_retry(func, max_retries3): for i in range(max_retries): try: return func() except requests.exceptions.HTTPError as e: if e.response.status_code 429: time.sleep(2 ** i random.uniform(0, 1)) else: raise raise Exception(still failed after retries)这套逻辑加上去之后会议创建的失败率明显下降。建议你在项目一开始就把这个重试机制写好不然后面补会很痛苦。6. 从“链接搬运工”到“会议助手”还能怎么玩说到最后所有对接的终极目标不是让机器人变成一个“复制粘贴会议号的工具”而是真正把会议全生命周期管理起来。6.1 联动飞书日历和会议室资源飞书日历的事件订阅和会议系统的联动是我觉得最值得深入的方向。你在飞书日历里新建一个日程选择“添加会议室”系统自动把日程里的事件ID传给后端后端调用腾讯会议API创建对应会议再把这个会议链接回写到日程描述或附加信息里。实现时要注意飞书日历事件的更新频率。有些同事会在一天内反复修改日程时间、地点、参会人如果每次都创建一个新腾讯会议很容易产生垃圾会议。我的建议是只在日历事件的status变成“confirmed”时创建会议后续时间变更时调用腾讯会议的“修改会议”接口而不是重新创建。6.2 把会议纪要写进飞书云文档顺便聊Dify授权会议结束后我们另一个需求是把录制文件链接、参会人列表、讨论要点整理到飞书云文档里。这个动作涉及飞书云文档API同时可以顺带用AI能力做会议纪要摘要。我当时参考Dify接飞书云文档的授权思路在Dify里新增了飞书云文档的凭证配置。这里有个重点Dify的回调地址和正常飞书自建应用的回调地址不是一个概念。你需要先在飞书开放平台把Dify的回调地址配置到重定向URI里然后在Dify后台填入飞书应用的App ID和App Secret点“授权”后才会跳到飞书让用户同意。如果回调URI没配哪怕App ID和App Secret完全正确也一样会报redirect_uri mismatch。拿到用户授权后Dify就能以用户身份读取云文档或创建新文档。这种模式和我自己写代码调docx:document接口是完全一致的只是Dify把OAuth流程封装好了。如果你不想用Dify直接用飞书云文档API也行只是要自己处理user_access_token的刷新。我的建议是小团队用现成低代码平台开发团队自己写接口复杂度完全不同。6.3 对后来者的落地建议如果让我重新做一遍这个飞书和腾讯会议对接我会按这个顺序推进先写一个命令行脚本手动传会议参数跑通腾讯会议API和飞书机器人。再做飞书事件订阅让群里机器人可以创建会议。然后用卡片代替纯文本加上取消、修改按钮。最后再上日历联动、云文档纪要、AI摘要。不要第一版就想做成一个完美的会议管理系统。先解决“每天复制粘贴会议号”这个小痛点已经可以显著提升团队效率。另外一点很值得注意机器人推送消息尽量用卡片不要用纯文本。卡片可以展示更多信息还能加按钮和状态长期维护成本反而更低。我在实际使用中发现同事对卡片的接受度远高于纯文本消息因为“看起来像是真正做出来的产品而不是脚本”。这一点在我后续几次迭代里每个人都给了正面反馈。最后分享一个小技巧在所有请求日志里把event_id、meeting_id、feishu_message_id三个ID串起来打印成一行。一旦出现“消息发出去了但会议没创建”之类的诡异问题你能很快通过日志定位到底是哪一环节丢了。这个习惯让我省了无数次排查时间。