ARTICLE DETAIL

资讯详情

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

飞书机器人定时消息推送完全指南:Webhook接入、签名校验与APScheduler实践

飞书机器人定时消息推送完全指南:Webhook接入、签名校验与APScheduler实践 1. 为什么我决定用飞书机器人做定时消息推送事情起源于一个很朴素的诉求每天上午十点团队群里需要准时出现一份前一天的线上数据简报每周五下午服务器巡检结果要自动发到运维群有时候还得把某个接口的监控截图作为图片消息推到群聊里。如果这一切都靠人工去做不是不行但实在是太浪费精力了而且人总有忘的时候。纯用 Python 去对接飞书开放接口自己写一套定时推送的脚本反而是最简单、最可控的方案。先说结论飞书自定义机器人本质上就是一个 Webhook 地址你用 HTTP POST 往这个地址丢一段固定格式的 JSON它就会把内容以机器人的身份发到群里。整个过程不需要申请 App、不需要审核应用权限只需要群里有人能添加自定义机器人拿到 Webhook 地址Python 代码就能直接对接。这也意味着这个东西非常适合个人开发者、小团队运维、以及自动化告警场景门槛比很多人想象的低得多。很多教程会推荐你直接装第三方库比如feishu或者lark-oapi但我的看法是如果只是发群消息通知根本没这个必要。直接用requests库就够了依赖少、逻辑透明、出了问题也好排查。这篇文章后面所有代码都只依赖requests和APScheduler前者负责发消息后者负责定时调度。读完你不仅能跑通定时发文本消息还能实现定时发图片、发富文本卡片并且避开我在实际开发中踩过的那些坑。2. 飞书自定义机器人的接入原理与第一个 Hello World2.1 创建机器人必须搞清楚的几个配置项在群里添加自定义机器人这件事不复杂但有几个细节会直接影响后面的开发我先把完整的路径写出来进入群聊 → 点击右上角设置 → 找到群机器人 → 添加机器人 → 选择自定义机器人 → 起名、设置头像 → 点击完成。关键来了完成创建后飞书会给你两个东西Webhook 地址形如https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx这是发送消息的唯一入口。签名校验密钥Secret创建过程中会让你选签名校验还是关键词校验如果你选了签名校验就会得到一个形如xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx的字符串这个就是加签密钥。我的建议是无论你的群是不是公开群都选签名校验不要嫌麻烦。原因很简单Webhook 地址如果泄露任何人都能往群里灌消息加上签名校验之后伪造请求的难度就大得多。关键词校验虽然配置简单但它在消息内容里强制要求包含某个词这会让你的自动化脚本内容受限非常不灵活。所以接下来的代码我全部以签名校验为例来实现。2.2 Webhook 消息格式与签名算法拆解飞书自定义机器人接收的请求体是一个 JSON最外层固定有一个msg_type字段表示消息类型不同类型对应不同的content结构。发送文本消息时请求体长这样{ msg_type: text, content: { text: hello world } }如果你开启了签名校验那么请求体还要额外带上timestamp和sign两个字段格式变成这样{ timestamp: 1700000000, sign: xxxxxxxxxxxxxxxx, msg_type: text, content: { text: hello world } }这里的timestamp是秒级 Unix 时间戳sign的计算方式是飞书这套接入体系里最核心的一环值得仔细讲。它的完整计算过程是取出字符串形式的timestamp在后面拼接上换行符\n再拼接你的 Secret 密钥形成一个新的字符串string_to_sign然后用 HMAC-SHA256 算法、使用这个拼接后的字符串作为 key注意这里是作为 HMAC 的 key 而不是消息体对空字符串进行加密最后对加密后的二进制结果做 Base64 编码得到最终的sign值。有些同学看到对空字符串加密会觉得奇怪但这确实是飞书文档的原话逻辑string_to_sign f{timestamp}\n{secret}然后hmac_code hmac.new(string_to_sign.encode(utf-8), digestmodhashlib.sha256).digest()最后sign base64.b64encode(hmac_code).decode(utf-8)。不要自己脑补成把整个消息体拿去加密我第一次就是犯了这个错误导致加签校验怎么都不通过。下面给出一个直接可用的 Python 函数import base64 import hashlib import hmac import time def gen_sign(secret: str) - tuple: timestamp str(int(time.time())) string_to_sign f{timestamp}\n{secret} hmac_code hmac.new( string_to_sign.encode(utf-8), digestmodhashlib.sha256 ).digest() sign base64.b64encode(hmac_code).decode(utf-8) return timestamp, sign这里有个容易出错的细节timestamp必须是字符串形式且和string_to_sign里用的完全一致。一旦你发送请求时把timestamp字段传成了整型或者生成的 time 和最终请求里的不一致签名就会不匹配。我习惯的做法是gen_sign一次性返回timestamp, sign两个值这样在组装请求体时直接用返回值最大程度避免不一致的问题。2.3 发送第一条文本消息的完整代码现在我们组合一下写一个send_text函数。我会把 Webhook 地址和 Secret 都作为函数参数传入而不是写死在代码里后面工程化改造那一节会说明这样做的理由。import requests def send_text(webhook: str, text: str, secret: str ) - dict: payload { msg_type: text, content: { text: text } } if secret: timestamp, sign gen_sign(secret) payload[timestamp] timestamp payload[sign] sign resp requests.post(webhook, jsonpayload, timeout10) result resp.json() return result if __name__ __main__: webhook https://open.feishu.cn/open-apis/bot/v2/hook/你的地址 secret 你的签名密钥 res send_text(webhook, 你好飞书机器人, secret) print(res)运行之后如果飞书返回的 JSON 是{code: 0, msg: success}那恭喜你机器人已经打通了。这里code0才代表成功不是 HTTP 状态码为 200 就万事大吉。飞书这套接口的 HTTP 状态码即使遇到业务错误也经常返回 200真正的成功与否要看响应体里的code字段这一点特别容易被初次对接的人忽略。还有一个使用上的细节群里的机器人发出的文本消息默认不会任何人。如果你希望消息能提醒到具体人可以在文本里插入at user_idxxxxxxxx/at标签其中user_id是飞书用户在通讯录里的 ID不是手机号也不是邮箱。还有一种更省事的办法是at user_idall/at就是所有人这个标签慎用打扰性极强建议只在重大告警场景使用。3. 定时任务的三条实现路线与选型对比机器人通了接下来的核心问题就是定时。实现定时的方案很多我不打算把所有方案都罗列一遍只讲三种我实际用过且觉得各有适用场景的方案操作系统自带的任务计划、Python 轻量调度库schedule、以及功能更完整的APScheduler。先看完对比再根据你自己的运行环境选。3.1 方案一Cron / 任务计划程序适合绝不玩花的场景如果你的脚本是在一台 Linux 服务器上跑而且只是定个时执行不需要动态增删任务最简单的方案就是写一个入口脚本然后用 Cron 来调用。# 每天上午 10:00 执行推送任务 0 10 * * * /usr/bin/python3 /opt/feishu_bot/main.py /var/log/feishu_bot.log 21在 Windows 上对应的就是任务计划程序操作路径是开始菜单搜索任务计划程序 → 创建基本任务 → 触发器选每天 → 操作选启动程序 → 程序填python.exe的绝对路径 → 参数填脚本绝对路径。Windows 计划任务相对容易忽略的一点是要在条件选项卡里取消勾选只有在计算机使用交流电源时才启动此任务否则笔记本没插电就不跑了。我前面说这个方案适合绝不玩花的场景是因为它的优点和缺点同样明显优点是完全不依赖 Python 进程常驻脚本跑完就结束不占内存即使脚本崩溃了第二天 Cron 还是会按时调它缺点是每次运行都是独立的对于需要在循环里动态等待然后发送多条消息这种逻辑就不太合适而且 Cron 的环境变量是精简过的如果你的 Python 脚本依赖某些自定义环境变量需要提前在脚本里设置好。3.2 方案二schedule 库轻量但有一个致命局限如果你不想碰系统级配置希望所有逻辑都在 Python 里完成那么schedule库是第一选择。pip install schedule基础用法长这样import time import schedule def job(): send_text(webhook, 定时消息, secret) schedule.every().day.at(10:00).do(job) while True: schedule.run_pending() time.sleep(1)我的评价是schedule的 API 设计确实比APScheduler亲切every().day.at()的语义读起来非常自然。但它的致命局限在于它本身是一个伪调度器它不会创建子进程、也不会在特定时间点自动唤醒它依赖你写一个无脑的while True循环不停地去调用run_pending()看看哪个任务到点了。这意味着你的 Python 进程必须一直活着一旦进程被 kill整个调度就消失了。同一个进程里跑多个任务时如果某个任务执行时间较长会阻塞其他任务容易造成调度迟到。没有持久化机制重启进程后需要重新注册所有任务。所以我个人认为schedule只适合开发测试、或者单任务非常轻量比如每天一条消息的生产场景。一旦任务数量超过两个或者任务执行时间可能超过一分钟我还是建议直接上APScheduler。3.3 方案三APScheduler生产环境的稳妥选择APScheduler是 Python 生态里最常用的任务调度库也是我最终的选择。它的核心概念有四个触发器Trigger、任务存储Job Store、执行器Executor和调度器Scheduler。触发器决定任务什么时候执行最常用的两种是IntervalTrigger每隔多少秒/分钟执行一次和CronTrigger类似 Cron 表达式指定每天的某个时刻执行。任务存储默认是内存存储进程重启后任务丢失也可以配置为数据库持久化适合需要记录历史任务的场景。执行器决定任务在线程池还是进程池里执行通常用默认的线程池即可。调度器组织以上三者最常用的是BlockingScheduler它会阻塞住主线程然后开始调度如果脚本里除了调度还有其他逻辑要跑可以用BackgroundScheduler。我的定时推送主程序核心逻辑如下from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger def morning_report(): text 早上好今日待办已生成请查收。 send_text(webhook, text, secret) if __name__ __main__: scheduler BlockingScheduler() scheduler.add_job( morning_report, CronTrigger(hour10, minute0, timezoneAsia/Shanghai), idmorning_report, misfire_grace_time60, coalesceTrue, max_instances1, ) scheduler.start()有四个参数对生产环境至关重要我可以说是踩过坑之后才理解的misfire_grace_time60表示如果因为某种原因任务没有准时触发比如上一次任务还没跑完、系统睡眠被唤醒那么在过期 60 秒之内仍然执行它超过这个窗口就直接放弃本次任务避免补跑造成通知混乱。coalesceTrue的含义是如果任务因为系统关机等原因积压了多次没有执行重新开机后只执行一次而不是把错过的那十次全部补跑一遍。这对于消息推送场景太重要了如果你不想屏幕上突然弹出二十条服务器已启动的提醒这个参数务必设为True。max_instances1则防止同一个任务在上一轮还没结束时又被调度触发一次避免出现两条线程同时发送重复消息。timezoneAsia/Shanghai是我额外强调的。APScheduler 默认使用本地时区如果你的服务器是 UTC 时区hour10就会变成北京时间下午六点。在 CronTrigger 里显式指定时区是防止定时不准最有效的手段。三条路线怎么选我整理成了下面这张表可以直接对着选方案适用场景优点缺点系统 Cron / 任务计划程序Linux 服务器、Windows 本机单脚本定时不依赖常驻进程系统级可靠无法动态管理多个任务环境变量需注意schedule 库开发测试、单任务轻量推送代码直观易上手需常驻进程任务阻塞会互相影响APScheduler多任务、定时规则复杂、生产环境功能完整支持持久化与线程池配置项偏多学习曲线稍陡4. 图文之外的硬需求图片、富文本卡片与高频发送限制4.1 发送图片消息的完整链路先拿 image_key再发消息飞书机器人发送图片消息不能像发文本那样直接把一张本地图片的路径塞进 JSON 里它要求必须先调用飞书开放平台的上传图片接口拿到一个image_key然后再将image_key作为消息内容发送。这一步很多第一次接触的人会卡住我详细展开说。首先要明确上传图片的接口https://open.feishu.cn/open-apis/im/v1/images需要tenant_access_token而这个 Token 的获取方式是调用https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal需要用到 App ID 和 App Secret。也就是说上传图片这一步并不依赖自定义机器人而是依赖飞书开放平台应用凭证。这可能是这篇文章里最容易让人混淆的地方自定义机器人只能发消息不能传文件、不能传图片凡是涉及资源上传的操作都需要单独申请一个企业自建应用用 App 凭证换 Token。如果你的场景只是发文本、发外链图片卡片可以跳过这一节前面介绍 Token 的部分只要你想发真正的本地图片文件就必须完成创建企业自建应用 → 获取 App ID/Secret → 换 Token → 上传图片 → 用返回的 image_key 发消息这条完整链路。创建自建应用的方法进入飞书开放平台 → 创建企业自建应用 → 拿到 App ID 和 App Secret。这里不需要发布应用只要创建一个草稿应用即可调用基础 API。获取tenant_access_token的 Python 代码如下import requests APP_ID 你的企业应用App ID APP_SECRET 你的企业应用App Secret def get_tenant_access_token() - str: resp requests.post( https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal, json{app_id: APP_ID, app_secret: APP_SECRET}, timeout10, ) data resp.json() if data.get(code) ! 0: raise RuntimeError(f获取 tenant_access_token 失败: {data}) return data[tenant_access_token]拿到 Token 之后上传图片的接口用的是multipart/form-data格式不是 JSON所以要用requests.post(..., files...)def upload_image(token: str, image_path: str) - str: with open(image_path, rb) as f: resp requests.post( https://open.feishu.cn/open-apis/im/v1/images, headers{ Authorization: fBearer {token}, }, data{image_type: message}, files{image: f}, timeout30, ) data resp.json() if data.get(code) ! 0: raise RuntimeError(f上传图片失败: {data}) return data[data][image_key]image_type参数这里填message表示图片用于消息发送。图片格式支持 jpg、png 等常见格式大小上限我记得是 10MB 左右实际使用中建议压缩到 1MB 以内否则上传速度和成功率都会受影响。拿到image_key之后发送图片消息的 payload 结构是def send_image(webhook: str, image_key: str, secret: str ) - dict: payload { msg_type: image, content: { image_key: image_key, }, } if secret: timestamp, sign gen_sign(secret) payload[timestamp] timestamp payload[sign] sign resp requests.post(webhook, jsonpayload, timeout10) return resp.json()这里有几个需要特别注意的环节image_key不是永久有效的据我实际观察它大约有 24 小时的有效期所以不能把 image_key 缓存下来重复用最好每次发图前现传现发。上传接口调用失败时先检查image_type是否正确、文件是否损坏、以及是否超过了文件大小限制。如果你只需要在消息里展示一张外部图片还有一个更轻量的方案直接在文本消息的text字段里嵌入![图片说明](https://xxx/xxx.png)这种 Markdown 图片语法但飞书机器人对普通文本消息里的 Markdown 图片支持比较弱我实测发现有些时候能显示有些时候显示不了所以不可控。要稳定地展示图片还是老老实实走msg_typeimage或者用后面说的交互卡片。4.2 富文本消息与交互卡片的 JSON 结构除了纯文本和图片飞书机器人还支持富文本消息post和交互卡片interactive。富文本的结构是一个二维数组content.post.zh-CN每一行是一个列表列表里的每个元素描述一个文本块或链接块def send_post(webhook: str, title: str, lines: list, secret: str ) - dict: # lines 示例: [[第一行文本], [[第二行加粗, bold], 普通文本]] content { post: { zh-CN: { title: title, content: [], } } } for line in lines: converted [] for item in line: if isinstance(item, str): converted.append({tag: text, text: item}) elif isinstance(item, tuple): tag, text item if tag bold: converted.append({tag: text, text: text, style: [bold]}) elif tag a: converted.append({tag: a, text: text, href: }) content[post][zh-CN][content].append(converted) payload { msg_type: post, content: content, } if secret: timestamp, sign gen_sign(secret) payload[timestamp] timestamp payload[sign] sign resp requests.post(webhook, jsonpayload, timeout10) return resp.json()交互卡片interactive实际上是一套 JSON 结构化配置它的核心思想是用飞书自研的卡片 JSON来描述整个卡片从头部、文本区块、分割线、按钮到图片全部是 JSON 对象。一个最简单的通知卡片长这样{ msg_type: interactive, card: { config: { wide_screen_mode: true }, header: { title: { tag: plain_text, content: 服务器告警 }, template: red }, elements: [ { tag: div, text: { tag: lark_md, content: **磁盘使用率**超过90%请尽快处理。 } }, { tag: hr }, { tag: note, elements: [ { tag: plain_text, content: 告警时间2025-01-01 10:00:00 } ] } ] } }我个人比较推荐在正式生产环境使用交互卡片因为它的排版天生就是给通知设计的有标题、有正文、有分割线、有注脚视觉上比重文本清爽得多。而且template字段支持red、orange、green、blue等颜色你可以根据消息的严重程度动态设置。飞书开放平台还提供了一个卡片搭建工具可以在线预览、调试 JSON调试完再粘贴到代码里效率会高很多。4.3 频率限制与重复消息的防抖策略飞书自定义机器人有一个硬性频率限制每个机器人每分钟最多发送 100 条消息超过之后会返回特定的限流错误码。在定时推送场景下我们通常不会跑到这个数字但我还是要提醒一句如果有批量通知的场景一定不要在 for 循环里直接调send_text发几百条而是要改成合并成一条大文本或者加 sleep 限速。我在实际项目中遇到过的一个比较隐性的问题是消息重复发送。原因多半不是频率限制而是你的定时任务在上一轮没跑完就被重复触发了一次或者因为网络超时你做了重试但飞书实际上已经收到了第一条消息于是两条一模一样的消息就出现在群里。针对这个问题我的处理办法有三板斧在发送函数里对同一批次的消息生成一个request_id如果超时重试使用相同的request_id飞书侧虽然没见过它有幂等字段但至少方便排查日志。发送前在 Redis 或者本地文件里记录一个最后发送时间戳如果离上次发送不足 5 秒直接丢弃本次发送。这个对网络抖动引起的重复重试特别有效。控制好重试次数最多重试 2 次且重试之间 sleep 2 秒。不要无限重试否则飞书返回限流错误后你还在那里硬试反而会在群里造成垃圾消息。5. 从能跑到稳定跑生产环境的工程化改造到这里你已经能手动跑通定时发送文本和图片了。但如果真的要让这个脚本每天无人值守地在服务器上跑还需要做一套工程化改造避免昨天还好好发消息今天就悄悄挂了这种情况发生。5.1 配置集中管理与 Webhook 安全我在 2.3 节特意把 Webhook 和 Secret 作为函数参数而不是写死在代码里就是为了在工程化阶段把它们挪到配置文件中。推荐用.env文件或者 JSON 配置文件不要放在.py文件里。一个很现实的原因是很多人会把这个脚本推到 GitHub 仓库做备份一旦 Webhook 地址泄露到公开仓库任何人都能向你的群发消息了。我用的做法是单独建一个config.py从环境变量读取敏感信息import os WEBHOOK os.getenv(FEISHU_WEBHOOK, ) SECRET os.getenv(FEISHU_SECRET, ) APP_ID os.getenv(FEISHU_APP_ID, ) APP_SECRET os.getenv(FEISHU_APP_SECRET, )然后写一个run.py在这个入口文件里加载配置、注册任务、启动调度器。这样做的好处是无论你将来是用 systemd、Docker 还是 GitHub Actions 来跑只需要注入环境变量即可代码一行都不用改。对于部署方式如果你在 Linux 上我强烈建议做成一个 systemd service这样即使进程崩了systemd 会自动帮你重启。它的最小配置大概是[Unit] DescriptionFeishu Bot Scheduler Afternetwork.target [Service] EnvironmentFEISHU_WEBHOOKxxx EnvironmentFEISHU_SECRETxxx EnvironmentFEISHU_APP_IDxxx EnvironmentFEISHU_APP_SECRETxxx ExecStart/usr/bin/python3 /opt/feishu_bot/run.py Restartalways RestartSec10 [Install] WantedBymulti-user.target别小看Restartalways这一行我见过太多定时脚本挂掉之后彻底失联的案例原因就是没人盯着进程。5.2 日志、异常告警与自愈机制定时任务最怕的问题就是凌晨三点脚本报错了但没人知道。所以要写日志而且要分级。print不是日志日志至少要包含时间、级别、模块、错误信息。Python 自带的logging已经够用如果你的任务逻辑复杂还可以接入loguru输出更友好。我的日志配置思路是所有正常发送记录写到info.log所有异常堆栈写到error.log同时在每次发送失败时把错误详情作为消息再次发到群里。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(module)s: %(message)s, handlers[ logging.FileHandler(/var/log/feishu_bot/info.log), logging.FileHandler(/var/log/feishu_bot/error.log), ], ) logger logging.getLogger(__name__)关于失败时通知到群里这个自愈机制这里有一个很特殊的逻辑发送失败后不要再调用同一个机器人地址去发告警消息如果它的原因是群被解散、机器人被移除你的告警也发不出去。所以我会准备一个备用的 Webhook 地址专门收异常告警。我在实际项目中准备了两个 Webhook一个在主群一个在专门用于告警的小群。主 Webhook 发送异常时备用 Webhook 就会顶上发一条主机器人异常请注意排查。5.3 多任务编排与消息内容模板化管理当你的定时任务多起来比如早上发日报、中午发待办、晚上发复盘每个任务的消息内容肯定不一样。我强烈建议把消息内容从代码里剥离出来做成模板化。最简单的做法是用字符串格式化def build_daily_report(date: str, order_count: int, revenue: float) - str: return ( f日报 {date}\n f今日订单量{order_count}\n f今日总营收{revenue:.2f} 元 )更进一步如果消息内容非常复杂可以用 Jinja2 模板把 Python 逻辑和消息文本完全分离。但在通知场景下字符串格式化基本就够用了。我自己的经验是写一套统一的消息构造器每种消息类型对应一个build_xxx函数返回的是 dict即 payload 里content部分再由统一的send函数负责加签、POST、重试。这样各个业务模块只需要关心消息内容不需要关心发送细节。5.4 保证定时推送精准性跨时区与夏令时问题前面提到时区问题这里再展开说一下。如果你的服务器不是中国地区的服务器Asia/Shanghai这个时区尤其要注意。CronTrigger如果不显式指定timezone它会使用调度器启动时从系统拿到的时区也就是date命令返回的时区。如果你服务器时区是 UTC而你想在北京时间上午 10 点发消息就需要把hour设为 2而且遇到夏令时可能还会出现偏差。最可靠的做法是在每个add_job里都显式指定timezoneAsia/Shanghai不要依赖系统时区。我还遇到过一个特别隐蔽的问题容器环境下Docker 镜像默认时区是 UTC即使你在宿主机设置了时区容器内也不会自动继承。如果你是在 Docker 里跑这个脚本建议在Dockerfile里加上ENV TZAsia/Shanghai并安装tzdata包。这个坑我记忆犹新宿主机时间完全正确但容器里的定时任务就是整整晚了 8 个小时。6. 常见问题排查一览按现象直接定位根因最后这部分写给正在调试中的人。我把自己遇到过高频问题整理成表格每一行都是现象 → 根因 → 解决办法的排查路径。如果你照着代码写的还是不触发先到这里对照一下。现象可能根因处理方式发送文本消息后群里没动静但接口没报错你没看响应里的code字段HTTP 200 不代表业务成功打印完整 resp.json()用code 0判断成功返回code: 19024或签名校验失败timestamp和sign不匹配或者timestamp被当成字符串拼接错误检查gen_sign中的timestamp与 payload 里的timestamp是否完全一致定时任务不触发时区不对CronTrigger没有显式指定timezone在add_job中设置timezoneAsia/Shanghai定时任务触发多次发送coalesce为默认值False积压任务被逐一执行设置coalesceTrue并设置max_instances1发送图片返回image_key不存在或失效上传和发送之间间隔时间过长image_key过期每次发送前现传现发缓存不要超过 10 分钟上传图片接口返回permission denied企业自建应用没有开启im:resource权限在飞书开放平台应用的权限管理中添加图片上传权限发布版本后生效消息里中文乱码请求体编码问题确认使用requests.post(webhook, jsonpayload)不要自己拼接 JSON 字符串再用data传发送消息后群里出现重复内容前一次请求超时你重试了但飞书实际已收到限制重试次数并增加防抖时间窗口这些坑里面签名校验失败和时区问题占了绝大多数。如果签名始终不对我建议你本地写个临时脚本把timestamp、sign和string_to_sign全部打印出来再对照飞书文档手动演算一遍问题基本就定位了。我自己第一次加签时就是因为把string_to_sign当成了要加密的明文把secret当成了 HMAC 的 key结果算出来的签名和飞书那边对不上后来才意识到飞书的规则是把整个timestamp\nsecret作为 key 去加密空字符串方向完全不同。图片上传链路如果报权限错误除了确认应用权限外还要留意你创建的自建应用是否发布过版本。开发阶段的权限修改默认不生效需要在飞书开放平台版本管理与发布里创建一个版本并发布新的权限才会真正生效。这个发布动作不会影响线上业务只是让你的应用在开发环境中也能使用新权限。如果你跑的是 APScheduler 并且使用BlockingScheduler请一定确认主线程没有后续代码被阻塞。之前有一个朋友照着别人的示例写把调度器start()放在函数中间后面又写了读取用户输入的input()结果程序一直在等调度器启动输入框永远不出来他一度以为是系统卡死了。最后我把整个项目最小的文件目录结构贴在下面方便你直接作为骨架搭建实际项目完全可以在这个基础上扩展feishu_bot/ ├── run.py # 入口文件初始化调度器并启动 ├── config.py # 从环境变量读取配置 ├── bot/ │ ├── __init__.py │ ├── sender.py # 封装发送文本、图片、富文本消息 │ ├── signer.py # 签名生成逻辑 │ ├── uploader.py # 上传图片获取 image_key │ └── tasks.py # 各业务定时任务的实现 └── logs/ ├── info.log └── error.log作为一个已经用这套方案跑了挺长一段时间的维护者我最想嘱咐你的一句话是先手动跑通发送再上定时调度最后再谈工程化。不要一上来就把 Cron 表达式和卡片 JSON 都堆在同一个文件里出了问题你根本不知道是调度的问题还是消息格式的问题。稳扎稳打这套东西很快就能成为你团队里最可靠的免费值班助理。
返回列表