ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

飞书与腾讯会议API对接:群内指令自动创建会议全流程

飞书与腾讯会议API对接:群内指令自动创建会议全流程 先交代一下背景。我所在的公司日常写字、拉群、审批、日程统统在飞书里完成但视频会议采购的是腾讯会议。两个平台各干各的本来井水不犯河水直到我发现每天最重复、最没价值的操作之一就是先在腾讯会议客户端创建会议、复制入会链接再切回飞书群把链接贴进去顺手还要在群里艾特一下参会人。一天重复四五次心情真的会变差。终于有一次周一开了四个会、贴了四次链接之后我决定不再忍了直接把飞书和腾讯会议对接起来。这个对接实践要解决的就是两件看起来简单、做起来琐碎的事在飞书群里发出创建腾讯会议的指令然后让机器人把会议号、入会链接和会议时间一键回传到群里。整套流程跑通之后团队不用再打开腾讯会议客户端也不用人工复制粘贴链接。后续我还顺手扩展了会前提醒和会议纪要回传但最核心的还是“飞书消息触发腾讯会议创建”这条链路。这篇文章会把整个对接过程拆开讲清楚包括飞书自建应用的创建、权限申请、事件订阅腾讯会议开放平台的接入、签名鉴权、创建会议接口最后落到一段可以直接改改就跑的工程代码上。适合什么人来参考企业内部系统集成开发、效率工具爱好者、正在做飞书或者腾讯会议API对接的工程师以及那些想在群里直接开会的运维和产品同学。1. 项目背景与整体方案设计1.1 为什么飞书和腾讯会议要打通先说需求来源。当时公司推行飞书作为统一工作入口文档、日程、机器人应用都往飞书迁。但视频会议因为合同和历史原因一直用的是腾讯会议。这时候问题就出来了如果说“飞书是办公室腾讯会议是会议室”那我的日常工作等于办公室和会议室之间隔着一条走廊每天来回跑。具体跑起来什么样每周例会、跟客户的临时沟通、跨部门对齐会都是在飞书群里临时约。约的过程是发起人打开腾讯会议客户端创建会议选好开始时间、结束时间生成链接再把链接复制回飞书群最后还要手动补充一句“明天10点入会”。如果会议多这个动作就会高频重复。而且入口不统一有人走客户端有人走网页版链接格式还不一样经常出现群里几串链接找不到哪个是当前会议的情况。自动化之后的目标就清晰了用户在飞书群里机器人附带一句“明天10点和客户对需求”机器人自动创建腾讯会议并把会议主题、会议号、入会链接、时间一起回传到群里。人只需要在飞书这个入口里完成所有操作腾讯会议从“主动打开的工具”变成“后台服务”。另外还有一个深层考虑——数据沉淀。人工贴链接的方式会议信息是不留痕的后续想统计某个月的会议量、平均时长、参与人没有数据可用。而通过API创建腾讯会议每次调用的参数和返回结果都可以存到自己的数据库里这为后面的会议数据统计和纪要归档打下了基础。这一点我觉得比省去几次复制粘贴更有价值。1.2 三条对接路线我为什么选了API直连做这个需求之前我大概调研了三种路线。路线A飞书机器人直接调用腾讯会议开放API。这也是我最终采用的方案。飞书提供机器人消息能力和事件订阅能力腾讯会议提供创建会议和查询会议的开放API中间用一个自建服务串起来。优点是链路完全可控数据、权限、运维都在自己手里而且两个平台的API能力都足够支撑这个场景。缺点是得自己写代码、自己部署一个公网可访问的服务。路线B用低代码平台中转。简道云、氚云这类平台都有飞书和腾讯会议的连接器可以通过可视化流程配置完成部分场景。优点是不用写代码但不是每个低代码平台的腾讯会议连接器都成熟只有一个创建会议动作还好一旦要处理消息事件、解析不同格式的会议指令低代码平台就非常别扭。而且这类平台的连接器往往有额外费用调用频率也有限制。路线C通过Webhook硬拼。腾讯会议本身有Webhook通知能力飞书也可以配置Webhook机器人但两者的Webhook都是单向的——飞书的Webhook只能收消息腾讯会议的Webhook只能往外发事件。这相当于两个单向阀门接不到一起要做双向联动还得自己写中间层。既然最终都要写代码那不如直接走官方API。三条路线对比下来直接选路线A。核心判断依据是我对接口成熟度的评估腾讯会议开放API支持创建会议、查询会议、修改会议、查询参会人飞书开放API支持接收消息事件、发消息、发卡片、创建日程和文档两个平台的能力交集恰好覆盖“群内指令创建会议并回传结果”这个闭环外加会前提醒和会后纪要。方案开发成本灵活性稳定性额外费用飞书机器人 腾讯会议API中高高无低代码平台中转低低中通常有Webhook硬拼中低中无1.3 整体数据流转一次“开会”指令的完整旅程整个链路我拆成了四个角色飞书客户端、飞书开放平台、自建服务、腾讯会议开放API。用户感知到的只是“在群里发一句指令”但背后经过了五次HTTP交互。用户发起指令之后飞书开放平台通过事件订阅把消息内容推送到我的自建服务。自建服务解析出会议主题、时间这些关键信息组装一个创建会议的请求体签名后调用腾讯会议开放API。腾讯会议创建成功返回meeting_id和join_url。自建服务再调用飞书API把会议信息以消息或卡片的形式发回对应的飞书群。这个链路里自建服务是绝对核心。为什么不能直接让飞书调用腾讯会议因为两个平台都是封闭的云服务没有公网上的直接“桥接器”腾讯会议不知道飞书群里发生了什么飞书也不知道腾讯会议创建了什么。必须有一个中间层它同时持有飞书应用的凭证和腾讯会议的签名密钥把两个平台传递的参数翻译成对方能理解的语言。我当时部署的是一台轻量云服务器公网上放一个Flask应用只需要一个端口接收飞书事件回调。整条链路是同步处理的飞书回调过来后服务先创建腾讯会议再把结果回复到群里耗时大概1到2秒。对于会议创建这种低频操作完全没有性能压力。2. 飞书侧接入应用创建、权限与凭证获取2.1 创建一个拥有机器人能力的自建应用飞书的开放体系里机器人不是一个独立账号而是“应用”的能力之一。所以第一步是创建一个企业自建应用并给应用开启机器人能力。操作路径是进入飞书开放平台在选择企业后点击“创建企业自建应用”填上应用名称和应用图标。这里有个细节应用名称和图标是员工在飞书里搜索、使用这个机器人时会看到的建议起一个直白的名字比如“会议助手”而不是用内部代号。创建成功后在“应用能力”页面里找到“机器人”点击启用。启用之后这个应用就拥有了一个“机器人”身份可以被拉进群、被、可以发消息。创建完成之后在“凭证与基础信息”页面能看到两个关键的字段App ID和App Secret。App ID是应用的公开标识相当于身份证号App Secret是签发的密钥相当于密码。之后凡是调用飞书开放平台API都需要用这两个值来换取访问凭证。务必把App Secret保存在服务端环境变量或者配置中心不要写进前端代码也不要提交到Git仓库。还有一个很容易被忽略的点自建应用区分企业版和个人版创建的时候一定要选对所属企业。如果选错后面申请权限和发布版本的时候会遇到审核流程不匹配的问题。企业内部自建应用走的是企业内部审核上线速度要快很多。2.2 权限配置与应用发布飞书的权限模型是申请制应用默认没有任何API权限需要明确申请才会被授予。这个设计刚开始觉得繁琐但实际用下来很合理——最小权限原则避免一个机器人拿太多不相关的API能力。我这次用到的权限主要是三个一是以应用的身份发送消息im:message:send_as_bot这个权限让机器人能往群里发消息二是读取群消息im:message:read用来接收用户机器人的指令三是获取群信息im:chat:read用来解析消息来源的群名称和群ID。如果后面要扩展日程同步还要申请日程相关的权限但初期这三个就够。权限申请提交之后需要企业管理员在管理后台审批。如果是测试阶段飞书提供了一个“测试企业”的环境也可以直接在测试企业里授权省去审批等待。审批通过后还有一个非常容易踩的坑权限不是申请完就立刻生效的必须在“版本管理与发布”里创建一个新版本提交发布出去运行时才会带上新权限。我最初就是申请完权限直接调接口报了一下午的no permission后来才反应过来是版本没有重新发。2.3 事件订阅与URL验证要让飞书在群里收到消息时通知我的服务需要配置“事件订阅”。这一步是整个飞书接入里最容易卡住的地方因为飞书要求在配置订阅URL的时候做一次双向验证。具体逻辑是在飞书开放平台的事件订阅页面填上你的回调URL比如https://your-server.com/webhook/feishu然后飞书会向这个URL发送一个POST请求请求体里带一个challenge字段。你的服务必须原样返回这个challenge值验证才算通过。这个机制本质上是为了确认URL背后是一个真实受控的服务而不是随便填的一个地址。第一次配置的时候我直接在浏览器里打开URL发现飞书提示验证失败后来才意识到需要服务端处理POST请求并返回JSON。正确的做法是在Web框架里写一个接口接收POST判断请求里有没有challenge字段有就原样回传。验证通过之后就可以订阅具体事件了。我订阅的是“接收消息”事件对应的事件类型是im.message.receive_v2。这里有个新老版本的区别老的v1版本事件结构简单但官方已经在逐步下架新开发统一用v2。v2版本的事件内容会多一层header和event的封装解析的时候要对应调整。如果担心回调内容被窃听飞书还支持在事件订阅里设置Encrypt Key回调请求体里的encrypt字段会用AES加密。加密解密的逻辑飞书官方SDK里有现成的类直接用就行。不是强制的但如果你对安全性要求高建议开启。2.4 获取tenant_access_token并发送消息到群里飞书API的访问凭证有两种tenant_access_token和user_access_token。tenant_access_token代表“应用自己”的身份适用于机器人发消息、读消息这些不需要用户维度的场景user_access_token代表“某个用户”的身份一般用于代用户创建日程、读取用户日历这类场景。我这次对接腾讯会议创建会议核心动作是机器人收到群消息后自动回复所以全程用tenant_access_token就够了。获取接口是POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal请求体传app_id和app_secret返回结果里带一个tenant_access_token。这个token的有效期官方文档标注是两个小时但实际测试大概是1小时50分钟左右代码里要加缓存和自动刷新不要每次调用都去申请一次。顺便提一个很多人在dify或者其他AI工具里接飞书云文档时遇到的困惑首次使用飞书云文档授权到底去哪拿凭证本质上也是走这一套token体系——如果工具是以应用身份访问飞书文档就会要求填App ID和App Secret然后工具自己调用上面的接口换token如果是以用户身份访问还需要走OAuth授权流程获取user_access_token。所以理解了飞书token体系这些工具的授权配置就不难了。发消息的接口是POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_typechat_id请求头带Authorization: Bearer {token}请求体里传receive_id群ID、msg_type消息类型和content消息内容。msg_type我用过text和interactive两种text是最简单的纯文本interactive是消息卡片支持按钮、Markdown、分栏等丰富布局。我最终回传会议信息用的是interactive卡片用户体验比纯文本好很多。import requests import json def get_tenant_access_token(app_id, app_secret): 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, timeout10) data resp.json() if data.get(code) 0: return data[tenant_access_token] raise Exception(f获取飞书token失败: {data[msg]}) def send_feishu_text(chat_id, text, token): url https://open.feishu.cn/open-apis/im/v1/messages?receive_id_typechat_id headers { Authorization: fBearer {token}, Content-Type: application/json; charsetutf-8, } body { receive_id: chat_id, msg_type: text, content: json.dumps({text: text}, ensure_asciiFalse), } resp requests.post(url, headersheaders, jsonbody, timeout10) return resp.json()3. 腾讯会议侧接入鉴权签名与创建会议3.1 开通腾讯会议开放平台API腾讯会议的开放平台和飞书不同API接入不是默认开放的。需要在腾讯会议官网找到开放平台入口用企业账号提交开通申请。个人免费版的腾讯会议账号是没有API权限的这是硬性条件。测试的时候腾讯会议开放平台提供了一个沙箱环境可以在里面模拟API调用不用真实创建会议。申请通过后开放平台的控制台里会生成一对密钥Secret ID和Secret Key。很多人第一次看到这两个名词会懵以为一个是账号一个是密码。实际上Secret ID是一个公开标识用来告诉腾讯会议“我是哪个应用”Secret Key才是真正参与签名计算的密钥相当于应用级别的私密钥匙。这两个值要妥善保存因为后面每次调用API都要用它们计算签名。这里要特别提醒一点腾讯会议开放平台的所有API调用几乎都要求请求头里带上四个字段X-TC-Key、X-TC-Nonce、X-TC-Timestamp、X-TC-Signature。缺一个或者算错一个服务端都会直接拒绝。我刚接入的时候就是因为漏了X-TC-Nonce这个随机串排查了很久才发现。3.2 腾讯会议的签名机制到底在签什么腾讯会议的签名机制本质上是HMAC-SHA256。签名的作用有两个一是确认调用方持有合法的Secret Key二是保证请求参数在传输过程中没有被篡改。这种思路在云厂商的开放API里非常常见飞书用的是直接用token腾讯会议则更接近腾讯云API的签名风格。签名串的拼接规则是固定的HTTP方法、请求路径、时间戳、随机串、请求体这五个部分用换行符拼成一个长字符串。然后以Secret Key作为密钥对这个长字符串做HMAC-SHA256摘要最后对摘要结果做Base64编码得到最终的签名。具体到创建会议的请求HTTP方法是POST请求路径是/v1/meetings时间戳是当前Unix秒级时间戳随机串是非ce可随机生成请求体就是准备发送给腾讯会议的JSON字符串。这里有个非常容易出错的地方请求体字符串必须和实际发送的请求体完全一致哪怕多一个空格、少一个字段签名校验都会失败。所以组装好签名之后真正发送请求时要直接把签名字符串对应的那串文本传过去而不是重新序列化一遍。import time import uuid import hmac import hashlib import base64 def generate_signature(method, uri, secret_key, timestamp, nonce, body_str): signing_str f{method}\n{uri}\n{timestamp}\n{nonce}\n{body_str} digest hmac.new( secret_key.encode(utf-8), signing_str.encode(utf-8), hashlib.sha256, ).digest() return base64.b64encode(digest).decode(utf-8)这个签名算法建议你写成一个独立函数后面所有腾讯会议API调用都复用。不要每个接口重复粘贴一份签名逻辑一旦要改算法或者补充字段改动量太大会很痛苦。3.3 创建腾讯会议接口与关键参数创建会议室的核心接口是POST https://api.meeting.qq.com/v1/meetings。这个接口的请求体字段比较多但核心就这几个userid创建者ID、topic会议主题、start_time开始时间、end_time结束时间、type会议类型1代表即时会议0代表预约会议。userid不是随便填的。首先需要确定这个userid对应的用户已经在腾讯会议企业账号下存在并且拥有创建会议的权限。一般建议用企业的管理员账号或者专门的API调用账号这样创建的会议归属清晰后续通过接口查询、修改、删除会议也会方便很多。userid可以在腾讯会议开放平台的管理后台查看也可以调用/v1/users/list接口查询。start_time和end_time的格式是Unix时间戳秒级注意是UTC时间。如果你直接传了一个本地时间比如东八区的2025-01-01 10:00:00对应的Unix时间戳那在接口这边得到的会议时间会偏差8小时。建议统一在服务端把本地时间转换成UTC时间戳再传。接口的响应里会返回meeting_id、meeting_code和join_url。meeting_id是会议的全局唯一标识后续查询、修改会议都靠它meeting_code是用户入会时输入的9位会议号join_url是入会链接可以直接嵌入飞书消息卡片里用户点击就能入会。import json import requests def create_tencent_meeting(userid, topic, start_time, end_time, secret_id, secret_key): uri /v1/meetings timestamp str(int(time.time())) nonce uuid.uuid4().hex body_dict { userid: userid, topic: topic, type: 1, start_time: str(start_time), end_time: str(end_time), } body_str json.dumps(body_dict, ensure_asciiFalse, separators(,, :)) signature generate_signature(POST, uri, secret_key, timestamp, nonce, body_str) headers { X-TC-Key: secret_id, X-TC-Nonce: nonce, X-TC-Timestamp: timestamp, X-TC-Signature: signature, Content-Type: application/json, } url fhttps://api.meeting.qq.com{uri} resp requests.post(url, headersheaders, databody_str.encode(utf-8), timeout10) return resp.json()3.4 userid从哪来userid是腾讯会议创建会议时的必填字段缺了它接口直接报“用户不存在”。很多人在这一步卡住其实是没搞清楚腾讯会议的userid和飞书的userid是两个体系、需要单独获取。腾讯会议的userid一般要从腾讯会议开放平台的成员管理里找。如果你是管理员登录开放平台后会看到企业成员列表每个成员都有一个唯一的userid。为了对接方便我建议单独创建一个机器人身份或者在已有管理员账号基础上固定使用同一个userid调用创建会议接口。这样所有自动创建的会议创建者都是同一个账号权限管理和日志排查都很清晰。如果一个账号的权限不够还可以通过/v1/users/list接口拉取企业用户列表拿到具体的userid。但这个接口本身也有权限要求开发阶段可以先在管理后台人工确认一个账号的userid把这个值配到配置文件里先跑通流程后面再考虑用户维度动态切换。4. 核心流程实现从飞书群消息到腾讯会议链接4.1 解析飞书消息指令的细节飞书把消息推送到自建服务的回调地址事件内容在event.message.content字段里这是一个JSON字符串需要先解析。content的结构根据消息类型不同而变化如果是text消息content长这样{text:会议助手 明天10点和客户对需求}。接收到的文本里会带上机器人的内容。这里有一个细节飞书的在文本中不是一个可见的符号而是一个open_id占位符形如at_open_id。所以不能直接拿整段文本去做关键词匹配需要先通过event.message.mentions数组找到机器人自己的open_id然后把文本里的占位符替换成空字符串剩下的才是用户真正输入的自然语言指令。我设计的指令格式很简单两个字段时间加主题中间用空格或者逗号分隔。比如“明天10点 和客户对需求”。考虑到用户习惯我还支持了一个更宽松的写法“开会 15:00 每周例会”。这里的关键不是做一个复杂的NLP而是用最简单的规则解析覆盖大多数使用场景。解析失败的时候默认创建半个小时后开始的会议并在回传消息里提醒用户注意时间。另外消息里有图片、表情等非文本类型时或者用户在群里发消息但根本没机器人时事件也会推到我的服务里。所以在处理逻辑的最前面要做一个判断只有message_type等于text并且mentions里包含机器人自己的open_id才进入创建会议的流程其他情况直接忽略。避免群里正常聊天内容也被误触发生成会议。4.2 组会议请求与异常兜底解析出指令之后紧接着就需要组装腾讯会议的请求体。这一步有几个参数需要提前算好开始时间、结束时间、会议主题。时间处理是我在这个项目里踩坑最多的部分。飞书推送的事件里没有直接给“用户当前时区”的信息而腾讯会议API要求传UTC时间戳。我的做法是在配置项里定一个默认时区比如Asia/Shanghai然后把用户输入的时间字符串解析成这个时区的datetime对象再用pytz或者zoneinfo转换成UTC时间最后转成Unix时间戳。如果用户没传时间我默认开始时间是当前时间加30分钟结束时间是开始时间加1小时。如果用户传了具体时间比如15:00那就用今天的15:00作为开始时间、16:00作为结束时间。这里要特别检查一个边界如果用户说的时间在今天已经过去了比如现在是16:30用户说15:00开会那就自动顺延到明天的15:00。这个判断逻辑虽然简单但对使用体验的提升非常明显因为真实情况下很多用户懒得写日期只写时间结果下午开会忘了带上午的时间。组装好请求体之后调用create_tencent_meeting函数。因为网络抖动、接口限流、参数错误都可能发生我统一用try-except包住捕获异常后往飞书群里发一条失败提示而不是让事件回调直接报错。用户看到失败提示可以自己修正指令再发一次比看服务日志友好得多。4.3 把会议卡片发回飞书群腾讯会议创建成功之后返回的join_url和meeting_code是用户最关心的两个信息。为了体验友好我没有用纯文本而是用飞书的消息卡片发回群里。消息卡片用msg_typeinteractivecontent是一个符合飞书卡片JSON格式的对象。卡片里我放了四个字段会议主题、开始时间、会议号、入会链接。入会链接做成一个可点击的按钮按钮文字是“点击入会”或“加入会议”。卡片右下角还能放一个“复制会议号”的辅助按钮方便用户把会议号发给外部参会人。构造卡片JSON的时候有一个坑飞书卡片支持的元素和属性非常丰富但版本不同支持的语法也不同。刚开始我按最新的card 2.0语法写结果在部分客户端上显示异常。后来干脆用官方“消息卡片搭建工具”可视化配置生成JSON再粘到代码里省去了反复查文档的麻烦。如果你也是第一次写飞书卡片非常推荐这个路子先拖拽生成再微调比手写JSON快很多。卡片里还有一个可选的做法把“添加到飞书日历”做成一个跳转链接。飞书开放平台支持通过URL Scheme直接创建日程不过这个能力需要额外的权限和配置。我没有在初版实现但这是一个很自然的扩展点。4.4 完整可运行的工程骨架为了让整体逻辑更清晰我按职责拆了几个文件。app.py负责接收飞书事件、处理URL验证、路由分发meeting_service.py负责腾讯会议签名和创建会议feishu_service.py负责飞书token申请和消息发送config.py放配置项。下面这个结构比较接近我当时跑通的版本去掉了一些业务细节和日志代码但主链路是完整的。# app.py import json from flask import Flask, request, jsonify import feishu_service import meeting_service import config app Flask(__name__) app.route(/webhook/feishu, methods[POST]) def feishu_callback(): body request.get_json(forceTrue) # 飞书URL验证 challenge body.get(challenge) if challenge: return jsonify({challenge: challenge}) header body.get(header, {}) event body.get(event, {}) if header.get(event_type) ! im.message.receive_v2: return jsonify({code: 0}) message event.get(message, {}) if message.get(message_type) ! text: return jsonify({code: 0}) mentions event.get(mentions, []) bot_open_id config.FEISHU_BOT_OPEN_ID if not any(m.get(id, {}).get(open_id) bot_open_id for m in mentions): return jsonify({code: 0}) content json.loads(message.get(content, {})) raw_text content.get(text, ) # 简单指令解析去掉占位符后的文本 clean_text raw_text.replace(fat_{bot_open_id}, ).strip() topic, start_time, end_time meeting_service.parse_meeting_request(clean_text) try: meeting meeting_service.create_tencent_meeting( useridconfig.TMEETING_USERID, topictopic, start_timestart_time, end_timeend_time, secret_idconfig.TMEETING_SECRET_ID, secret_keyconfig.TMEETING_SECRET_KEY, ) token feishu_service.get_tenant_access_token( config.FEISHU_APP_ID, config.FEISHU_APP_SECRET ) chat_id message.get(chat_id) feishu_service.send_meeting_card(chat_id, meeting, token) except Exception as e: token feishu_service.get_tenant_access_token( config.FEISHU_APP_ID, config.FEISHU_APP_SECRET ) feishu_service.send_feishu_text( message.get(chat_id), f创建会议失败: {str(e)}, token ) return jsonify({code: 0}) if __name__ __main__: app.run(host0.0.0.0, port8080)# meeting_service.py import json import re import time import uuid from datetime import datetime, timedelta from zoneinfo import ZoneInfo import requests from signature_util import generate_signature DEFAULT_TZ ZoneInfo(Asia/Shanghai) def parse_meeting_request(text): # 简单解析尝试匹配时间 HH:MM其余部分作为主题 time_match re.search(r(\d{1,2})[:](\d{2}), text) if time_match: hour, minute int(time_match.group(1)), int(time_match.group(2)) start_local datetime.now(DEFAULT_TZ).replace( hourhour, minuteminute, second0, microsecond0 ) else: start_local datetime.now(DEFAULT_TZ) timedelta(minutes30) if start_local datetime.now(DEFAULT_TZ): start_local timedelta(days1) end_local start_local timedelta(hours1) topic re.sub(r\d{1,2}[:]\d{2}, , text).strip() or 快捷会议 start_ts int(start_local.timestamp()) end_ts int(end_local.timestamp()) return topic, start_ts, end_ts def create_tencent_meeting(userid, topic, start_time, end_time, secret_id, secret_key): uri /v1/meetings timestamp str(int(time.time())) nonce uuid.uuid4().hex body_dict { userid: userid, topic: topic, type: 1, start_time: str(start_time), end_time: str(end_time), } body_str json.dumps(body_dict, ensure_asciiFalse, separators(,, :)) signature generate_signature(POST, uri, secret_key, timestamp, nonce, body_str) headers { X-TC-Key: secret_id, X-TC-Nonce: nonce, X-TC-Timestamp: timestamp, X-TC-Signature: signature, Content-Type: application/json, } url fhttps://api.meeting.qq.com{uri} resp requests.post(url, headersheaders, databody_str.encode(utf-8), timeout10) if resp.status_code ! 200: raise Exception(f腾讯会议接口异常: {resp.status_code} {resp.text}) return resp.json()[meeting_info_list][0][meeting_info]# signature_util.py import base64 import hashlib import hmac def generate_signature(method, uri, secret_key, timestamp, nonce, body_str): signing_str f{method}\n{uri}\n{timestamp}\n{nonce}\n{body_str} digest hmac.new( secret_key.encode(utf-8), signing_str.encode(utf-8), hashlib.sha256, ).digest() return base64.b64encode(digest).decode(utf-8)# feishu_service.py import json import time import requests token_cache {token: , expire_at: 0} def get_tenant_access_token(app_id, app_secret): if token_cache[token] and token_cache[expire_at] time.time() 60: return token_cache[token] 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}, timeout10 ) data resp.json() if data.get(code) ! 0: raise Exception(f获取飞书token失败: {data[msg]}) token_cache[token] data[tenant_access_token] token_cache[expire_at] time.time() data[expire] - 120 return token_cache[token] def send_meeting_card(chat_id, meeting, token): url https://open.feishu.cn/open-apis/im/v1/messages?receive_id_typechat_id headers { Authorization: fBearer {token}, Content-Type: application/json; charsetutf-8, } card { config: {wide_screen_mode: True}, header: { title: {tag: plain_text, content: meeting[topic]}, template: blue, }, elements: [ {tag: div, text: {tag: lark_md, content: f会议号**{meeting[meeting_code]}**}}, {tag: div, text: {tag: lark_md, content: f入会链接[点击入会]({meeting[join_url]})}}, { tag: action, actions: [ { tag: button, text: {tag: plain_text, content: 复制会议号}, type: primary, value: {meeting_code: meeting[meeting_code]}, } ], }, ], } body { receive_id: chat_id, msg_type: interactive, content: json.dumps(card, ensure_asciiFalse), } resp requests.post(url, headersheaders, jsonbody, timeout10) return resp.json()这套代码在我当时的环境里跑通之后整个团队的体验变化非常直观。以前“创建会议贴链接”要30秒到1分钟现在群里发一句话1到2秒后会议卡片就出现了。5. 常见问题与排查技巧实录5.1 飞书事件订阅验证不通过90%是这里的问题配置飞书事件订阅URL的时候验证失败是出现频率最高的报错。如果你在飞书开放平台点“验证”直接红了先别急着怀疑代码逻辑优先检查三件事。第一URL是不是公网可访问的并且是HTTPS或者HTTP都能通本地开发跑localhost肯定不行需要用内网穿透工具映射到公网。穿透工具有免费版但地址会变所以开发测试时每次都要更新飞书后台的URL。生产环境还是建议固定域名加Nginx反代稳定很多。第二接口是不是只处理了POST飞书验证回调是POST请求如果你在浏览器直接打开URL看到404不代表接口有问题要看服务日志里请求有没有进来。最直接的办法是在回调函数第一行加个print(request.get_json())然后看服务器日志。第三返回的JSON格式是否正确验证成功必须返回{challenge: xxx}除了这个键之外最好不要带其他字段。如果走了加密通道也就是配置了Encrypt Key那challenge是加密在encrypt字段里的需要先解密才能取到明文challenge。很多人忽略这一点配置了Encrypt Key却用明文方式解析验证永远过不去。我当时卡了最久的地方是飞书新版事件回调的外层结构从{challenge: xxx}变成了{schema: 2.0, header: {...}}这种结构导致我解析challenge的代码第一次没匹配到。现在的处理方式是兼容两种结构先解析最外层的challenge如果没有再从header里找稳妥很多。5.2 腾讯会议签名报错时间戳、请求体和密钥的三方校验调用腾讯会议API最常见的是401错误也就是签名校验失败。排查这个问题的思路是把签名串原样打印出来和官方调试工具算出来的结果逐字符对比。一般就三个原因。第一个原因是服务器时钟不准。签名机制里时间戳参与计算而且服务端会校验时间戳和当前时间差是否在可接受范围内。如果你的服务器时间偏差超过5分钟签名就算正确也会被拒绝。解决办法是配置NTP时钟同步别用一台时间偏了好几个月的机器。第二个原因是请求体不一致。我遇到过的情况是签名时用的body_str用json.dumps生成的但发送请求时用requests库的json参数Python的requests会重新序列化一遍序列化结果可能和签名时不完全一样尤其是中文字符的ensure_ascii、字典键的顺序都会导致签名串和服务端验签串对不上。解决方法是签名用的body_str原封不动地放进data参数里发送并且设置Content-Type为application/json。第三个原因是Secret Key配错。这里有一个特别容易搞混的点签名用的密钥是Secret Key不是Secret ID。如果你把Secret ID当成密钥去HMAC结果必然不对。腾讯会议的密钥体系分两层确实比较容易搞混淆。5.3 摄像头不能用这个锅不一定在腾讯会议这个话题我要单独拿出来说因为真的有人把“摄像头调不出来”归咎于腾讯会议客户端。包括在对接过程中也有同事反馈说“腾讯会议不能使用电脑自带摄像头吗”。实际情况是腾讯会议完全支持电脑自带摄像头问题一般出在设备占用和系统权限上。设备占用是最常见的原因。Windows上如果已经打开了一个视频软件比如钉钉、飞书视频、微信视频或者浏览器正在用摄像头做在线会议再开腾讯会议摄像头就会提示被占用。因为同一个硬件在同一时间只能被一个进程抢占。解决的办法就是先退出其他占用摄像头的程序然后再重新进入腾讯会议。系统权限也要查一下。macOS的隐私设置里有摄像头权限列表如果腾讯会议不在允许列表里摄像头画面就是黑的。Windows 10以上同理在设置-隐私-摄像头里检查允许桌面应用访问摄像头是否开启。还有一类是驱动问题多见于老款笔记本需要更新摄像头驱动这个只能根据具体设备型号去官网找驱动。总体排查顺序建议先退其他软件再查系统权限最后考虑驱动。5.4 时区差了8小时Unix时间戳的坑整个对接过程中关于时间我踩了不少坑在这里集中说一下。腾讯会议API的start_time和end_time是Unix时间戳单位是秒而且是UTC标准。Unix时间戳本身没有时区概念它就是自1970年1月1日UTC以来的秒数。问题出在把“用户输入的本地时间”转换成时间戳这一步。如果你用datetime.now()拿到本地时间再直接调用timestamp()这个转换是没问题的因为timestamp()会基于系统本地时区换算成UTC时间戳。但如果你手动做字符串拼接比如把2025-01-01 10:00:00直接当成UTC时间戳取秒数那就会多算8小时。排查时区问题有个很有效的办法创建完会议后在腾讯会议后台看这个会议的时间显示。如果显示的时间比预期早或者晚8小时说明时间戳转换环节有偏差。还有一个办法是会议创建成功后在回传飞书的卡片里显示本地时间让用户来反馈时间是否准确这样可以不断积累日志反向调整解析逻辑。我自己后来干脆写了一个公共函数所有时间转换都走同一个函数不在业务代码里散落着各种各样的datetime操作。这样一旦发现时区有问题只需要改一个地方。5.5 接口限流与重试策略飞书和腾讯会议的开放API都有访问频率限制。飞书侧按应用维度限流单位时间内请求次数过多会返回HTTP 429和错误码。腾讯会议侧的限流策略更严格创建会议接口如果调用过于频繁会被临时封禁一小段时间。我在自建服务里加了两层防护。第一层是在调用腾讯会议接口之前判断当前时间距离上次调用是否小于一个最小间隔比如5秒。如果用户连发两次开会指令第二次直接提示“正在创建会议请稍候”而不是真的去调接口。第二层是封装一个带指数退避的重试函数。遇到网络错误和HTTP 5xx错误时最多重试3次每次间隔按2的指数递增比如1秒、2秒、4秒。还有一点要特别注意飞书事件回调的请求如果返回超时飞书会重试推送事件。这会导致同一句“开会”指令被推送两次如果服务端没有做幂等处理腾讯会议会被创建出两个重复会议。我的做法是在Redis里按消息ID做去重处理过的消息ID直接忽略。如果没有Redis用一个本地字典加过期时间也能实现基础去重只不过多实例部署时会有问题。5.6 权限不足与常见错误速查把我在实际对接过程中遇到的典型错误和解决办法整理成一张表方便你直接对照排查。错误现象可能原因处理建议飞书接口返回no permission应用未申请对应权限或权限未随新版本发布重新申请权限并发布新版本确认发布成功飞书事件订阅URL验证失败URL不可访问、未处理challenge、加密Key未解密检查公网可达性兼容多种challenge结构开启加密时先解密腾讯会议接口401时间戳偏差、签名串不一致、Secret Key配错同步NTP时钟打印签名串对比区分Secret ID和Secret Key腾讯会议接口403API权限未开通或账号权限不足确认企业账号已开通API能力检查userid权限创建会议时间不对Unix时间戳换算时区错误统一走UTC时间戳转换回传卡片显示本地时间会议创建成功但无法入会腾讯会议账号未激活或被禁用检查腾讯会议企业后台账号状态重复创建同主题会议飞书事件回调重试触发基于消息ID做去重处理写在最后这个对接后续还能怎么扩展整个“飞书-腾讯会议对接”做完到现在团队的会议创建流程已经彻底从客户端转移到了飞书群里。最近我又加了一个每天早上的定时任务扫描当天所有自动创建的会议提前30分钟在飞书群里发提醒卡片附带会议链接。前两周上线后迟到率明显下降这个效果是我预期之外的。如果你也想做类似的事情我的建议是先把主链路跑通不要一上来就想着把所有功能都做完。就按“群里发指令创建会议、机器人回传链接”这一个闭环来哪怕先不支持时间解析直接固定默认开会时间也可以让流程的先跑起来。跑通之后再慢慢加时间识别、卡片美化、会前提醒、会议纪要归档。还有一个可以尝试的方向是把飞书云文档和会议纪要串起来。腾讯会议的转写文本拿到之后通过飞书云文档API生成一篇在线文档然后把文档链接发到会议卡片里。我在做AI知识库相关项目时试过类似思路飞书云文档的授权凭证体系就是前面讲的token那一套理解了底层逻辑扩展其实很顺畅。最后再分享一个工程上的小技巧把飞书和腾讯会议的API凭证统一放到一个配置中心管理不要让每个人各存一份。因为这两个平台的密钥泄漏都可能导致会议被乱建、消息被乱发权限影响面很大。我当时就吃过亏有一次同事把Secret Key贴到了聊天群里后来只能重新生成密钥代价不大但很折腾。对接类项目密钥管理从一开始就要规范起来。
返回列表