events
feishu.events
¶
EventDispatcher
¶
将飞书事件按类型分发给已注册的异步处理函数。
通过 on 装饰器按事件类型注册处理函数,
使用 "*" 注册可匹配所有事件的兜底处理函数。
dispatch 会先运行精确匹配的处理函数,
再运行兜底处理函数。
单个处理函数抛出的异常会被捕获并记录,不会中断其余处理函数(含 "*" 兜底);可通过
on_error 注册全局异常处理函数集中上报。
若构造时传入 SeenStore,分发前会基于 Event.event_id 去重,避免飞书重试导致重复处理。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
SeenStore | None
|
事件去重存储;为 |
None
|
|
Logger | None
|
处理函数异常的日志器;缺省使用名为 |
None
|
飞书文档
示例:
源代码位于: feishu/events/dispatcher.py
| Python | |
|---|---|
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
on
¶
on(event_type: str) -> Callable[[Handler], Handler]
注册指定事件类型的处理函数(装饰器)。
同一类型可注册多个处理函数,按注册顺序执行。使用 "*" 注册兜底处理函数,
它会在所有事件的精确匹配处理函数之后执行。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
str
|
事件类型,如 |
必需 |
返回:
| 类型 | 描述 |
|---|---|
Callable[[Handler], Handler]
|
接收处理函数并将其注册的装饰器;原函数会被原样返回。 |
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/dispatcher.py
on_error
¶
on_error(handler: ErrorHandler) -> ErrorHandler
注册全局异常处理函数(仿 Slack Bolt 的 @app.error)。
当任一事件处理函数抛出异常时,框架会先记录日志,再调用此处理函数用于集中上报或返回兜底结果。
处理函数接收异常与对应事件;若其返回非 None 值且本次分发尚无其他结果,则作为返回值
(例如卡片回调可借此返回错误 toast)。后注册的会覆盖先注册的。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
ErrorHandler
|
形如 |
必需 |
返回:
| 类型 | 描述 |
|---|---|
ErrorHandler
|
原处理函数(便于作装饰器使用)。 |
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/dispatcher.py
dispatch
async
¶
将事件分发给匹配的处理函数并返回结果。
先执行与 event.event_type 精确匹配的处理函数,
再执行以 "*" 注册的兜底处理函数。若配置了去重存储且事件已处理过,则直接返回 None,
不执行任何处理函数。
单个处理函数抛出的异常会被捕获、记录,并交由可选的全局异常处理函数处理,随后继续执行其余 处理函数,确保一个出错的处理函数不会中断其他处理函数或丢失卡片回调的 ACK。
返回值取第一个非 None 的处理函数结果,通常用于卡片回调返回 {"toast": ..., "card": ...}。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
Event
|
待分发的事件。 |
必需 |
返回:
| 类型 | 描述 |
|---|---|
dict | None
|
首个非 |
示例:
源代码位于: feishu/events/dispatcher.py
Event
¶
飞书事件载荷的统一视图,同时兼容 1.0 与 2.0 两种事件格式。
飞书早期(1.0)事件将类型、uuid、ts、token 放在顶层,事件正文在 event 中;
新版(2.0)事件则将这些元信息收敛到 header 字段。Event 屏蔽了二者差异,
使调用方无需关心具体 schema 即可读取 event_type、
event_id 等字段。
通常无需直接构造,应使用 from_payload 由原始载荷推断。
飞书文档
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/envelope.py
| Python | |
|---|---|
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
event_type
property
¶
event_type: str
事件类型,例如 im.message.receive_v1。
2.0 取自 header.event_type,1.0 取自 event.type;缺失时返回空字符串。
event_id
property
¶
event_id: str
事件唯一标识,用于去重。
2.0 取自 header.event_id,1.0 取自顶层 uuid。始终返回 str,缺失时返回空字符串
而非 None,因此调用方无需进行 None 判空。
create_time
property
¶
create_time: str | None
事件产生时间戳(毫秒,字符串形式)。
2.0 取自 header.create_time,1.0 取自顶层 ts;缺失时返回 None。
token
property
¶
token: str | None
事件头中的校验 Token(Verification Token)。
用于事件来源校验,并非更新卡片所需的凭证。2.0 取自 header.token,1.0 取自顶层 token。
body
property
¶
body: NestedDict
from_payload
classmethod
¶
从原始事件载荷构造 Event 并自动推断 schema 版本。
当载荷含有 "schema": "2.0" 或顶层存在 header 字段时判定为 2.0,否则视为 1.0。
传入的 dict 会被包装为 NestedDict 以支持点号取值。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
dict[str, Any]
|
解密、解析后的事件载荷字典。 |
必需 |
返回:
| 类型 | 描述 |
|---|---|
Event
|
统一封装后的事件对象。 |
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/envelope.py
InMemorySeenStore
¶
基于进程内存、带 TTL 的 SeenStore 实现。
将已处理的 event_id 连同过期时间存入字典,每次访问时清理过期项。适合单进程部署与测试;
多副本部署时各进程内存互不共享,应改用基于共享存储的实现。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
float
|
记录的存活时长(秒),超过后视为未见过。默认 |
3600.0
|
|
Callable[[], float]
|
单调时钟函数,默认 |
monotonic
|
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/idempotency.py
add
async
¶
原子地认领 event_id:此前未标记(或已过期)则标记并返回 True,否则返回 False。
将 seen 检查与 mark 标记合并在同一把锁内完成,消除二者之间的检查-标记竞态——
并发投递的重复事件中只有一个会得到 True。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
str
|
待认领的事件标识。 |
必需 |
返回:
| 类型 | 描述 |
|---|---|
bool
|
首次认领返回 |
源代码位于: feishu/events/idempotency.py
seen
async
¶
SeenStore
¶
Bases: Protocol
事件去重存储的协议接口。
飞书在投递超时时会重试推送,导致同一 event_id 多次到达。实现本协议即可为接收器
(create_event_route、
create_card_route)或
EventDispatcher 提供幂等保证。
通过 InMemorySeenStore 即得到一个内置实现,
生产环境可改用基于 Redis 等共享存储的实现。
本协议使用 runtime_checkable,可用 isinstance 进行结构化校验。
飞书文档
源代码位于: feishu/events/idempotency.py
seen
async
¶
verify_signature
¶
verify_signature(timestamp: str, nonce: str, encrypt_key: str, raw_body: bytes, signature: str) -> bool
校验飞书事件推送的签名。
飞书在请求头中携带 X-Lark-Request-Timestamp、X-Lark-Request-Nonce 与 X-Lark-Signature。
签名为 sha256(timestamp + nonce + encrypt_key + raw_body) 的十六进制摘要。
本函数使用常量时间比较以避免计时攻击。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
str
|
请求头 |
必需 |
|
str
|
请求头 |
必需 |
|
str
|
应用配置的 Encrypt Key。 |
必需 |
|
bytes
|
HTTP 请求的原始字节体(必须在解析 JSON 之前读取,不能被改动)。 |
必需 |
|
str
|
请求头 |
必需 |
返回:
| 类型 | 描述 |
|---|---|
bool
|
签名匹配返回 |
飞书文档
示例:
源代码位于: feishu/signature.py
decrypt
¶
decrypt(encrypt_key: str, b64_ciphertext: str) -> bytes
解密飞书事件的 encrypt 密文。
飞书在开启加密推送后,会将事件体加密为一段 Base64 字符串放在 encrypt 字段中。
其算法为 AES-256-CBC:密钥取 sha256(encrypt_key),前 16 字节为 IV,采用 PKCS7 填充。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
str
|
应用在飞书开放平台配置的 Encrypt Key。 |
必需 |
|
str
|
|
必需 |
返回:
| 类型 | 描述 |
|---|---|
bytes
|
解密并去除填充后的原始明文字节(通常是一段 JSON)。 |
引发:
| 类型 | 描述 |
|---|---|
[feishu.errors.FeishuCryptoError][]
|
当 Base64 非法、密文长度不足一个 AES 块, 或密钥错误导致去填充失败时抛出。 |
飞书文档
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/crypto.py
create_card_route
¶
create_card_route(dispatcher: EventDispatcher, *, path: str = '/feishu/card', encrypt_key: str | None = None, verification_token: str | None = None, seen_store: Any = _DEFAULT_SEEN_STORE, max_age_seconds: float | None = 300, now: Callable[[], float] = time) -> Route
创建处理飞书卡片交互回调的 Starlette POST 路由。
与 create_event_route 不同,本路由会
**同步**等待分发器执行(不使用后台任务),并将处理函数返回的 {toast, card} 字典
作为同步 JSON 响应返回,以满足飞书对卡片交互约 3 秒的响应时限。当处理函数返回 None 时,
响应为 200 {}。
安全模型与 create_event_route 一致:
url_verification握手仅通过内层verification_token鉴权(签名豁免在此生效)。- 其余事件在配置了
encrypt_key时,必须携带并经 SignatureVerifier 通过X-Lark-Signature校验,且时间戳须在max_age_seconds时间窗内(防重放);缺失、过期或非法签名将返回 401,且处理函数不会被调用。
由于飞书在超时时会重试卡片回调,命中 seen_store 的重复事件会直接返回 {},避免重复触发副作用。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
EventDispatcher
|
事件分发器。 |
必需 |
|
str
|
路由路径,默认 |
'/feishu/card'
|
|
str | None
|
应用配置的 Encrypt Key;设置后启用解密与签名强校验。 |
None
|
|
str | None
|
握手校验 Token;设置后会校验 |
None
|
|
Any
|
事件去重存储;缺省时使用新建的
InMemorySeenStore,
从而开箱即用地提供去重/防重放保护;显式传入 |
_DEFAULT_SEEN_STORE
|
|
float | None
|
签名请求允许的最大时延(秒),默认 |
300
|
|
Callable[[], float]
|
返回当前 epoch 秒的可调用对象,默认 time.time;可注入以编写确定性测试。 |
time
|
返回:
| 类型 | 描述 |
|---|---|
Route
|
可挂载到 Starlette 应用的 [Route][starlette.routing.Route]。 |
飞书文档
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/receiver.py
| Python | |
|---|---|
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | |
create_event_app
¶
create_event_app(dispatcher: EventDispatcher, *, event_path: str = '/feishu/event', card_path: str | None = '/feishu/card', encrypt_key: str | None = None, verification_token: str | None = None, seen_store: Any = _DEFAULT_SEEN_STORE, card_seen_store: Any = _DEFAULT_SEEN_STORE, max_age_seconds: float | None = 300, now: Callable[[], float] = time) -> Starlette
返回一个可独立运行、处理飞书 Webhook 推送的 Starlette 应用。
始终在 event_path 挂载事件路由;当 card_path 不为 None 时,额外在该路径挂载卡片回调路由。
全部安全与路由逻辑分别委托给 create_event_route
与 create_card_route,因此默认即带有
签名新鲜度(防重放)时间窗与去重保护。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
EventDispatcher
|
事件分发器。 |
必需 |
|
str
|
事件路由路径,默认 |
'/feishu/event'
|
|
str | None
|
卡片回调路由路径,默认 |
'/feishu/card'
|
|
str | None
|
应用配置的 Encrypt Key;设置后启用解密与签名强校验。 |
None
|
|
str | None
|
握手校验 Token;设置后会校验 |
None
|
|
Any
|
事件路由的去重存储;缺省时各路由自建
InMemorySeenStore,
显式传入 |
_DEFAULT_SEEN_STORE
|
|
Any
|
卡片路由的去重存储;语义同 |
_DEFAULT_SEEN_STORE
|
|
float | None
|
签名请求允许的最大时延(秒),默认 |
300
|
|
Callable[[], float]
|
返回当前 epoch 秒的可调用对象,默认 time.time;可注入以编写确定性测试。 |
time
|
返回:
| 类型 | 描述 |
|---|---|
Starlette
|
已挂载相应路由的 [Starlette][starlette.applications.Starlette] 应用。 |
飞书文档
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/receiver.py
create_event_route
¶
create_event_route(dispatcher: EventDispatcher, *, path: str = '/feishu/event', encrypt_key: str | None = None, verification_token: str | None = None, seen_store: Any = _DEFAULT_SEEN_STORE, max_age_seconds: float | None = 300, now: Callable[[], float] = time) -> Route
创建处理飞书事件推送的 Starlette POST 路由。
该端点的处理流程:
- 先读取原始请求体字节(签名校验与 AES 解密都依赖未经改动的原始字节)。
- 当配置了
encrypt_key且请求头包含X-Lark-Signature时, 经 SignatureVerifier 校验签名 并校验时间戳新鲜度(重放时间窗),记录校验结果。 - 若请求体被
encrypt包裹,则先行解密。 - 处理
url_verification握手:握手仅通过内层verification_token鉴权, 不要求 MAC 签名(签名豁免仅在此处生效)。 - 其余正常事件:当配置了
encrypt_key时,签名必须存在、时间戳在max_age_seconds时间窗内且校验通过;缺失签名或时间戳过期将返回 401 (防止 Webhook 注入与重放攻击绕过)。 - 通过
seen_store去重(默认即开启),其余事件以后台任务(BackgroundTask) 异步分发,端点立即返回200 {}。
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
|
EventDispatcher
|
事件分发器。 |
必需 |
|
str
|
路由路径,默认 |
'/feishu/event'
|
|
str | None
|
应用配置的 Encrypt Key;设置后启用解密与签名强校验。 |
None
|
|
str | None
|
握手校验 Token;设置后会校验 |
None
|
|
Any
|
事件去重存储;缺省时使用新建的
InMemorySeenStore,
从而开箱即用地提供去重/防重放保护;显式传入 |
_DEFAULT_SEEN_STORE
|
|
float | None
|
签名请求允许的最大时延(秒),默认 |
300
|
|
Callable[[], float]
|
返回当前 epoch 秒的可调用对象,默认 time.time;可注入以编写确定性测试。 |
time
|
返回:
| 类型 | 描述 |
|---|---|
Route
|
可挂载到 Starlette 应用的 [Route][starlette.routing.Route]。 |
飞书文档
示例:
| Python Console Session | |
|---|---|
源代码位于: feishu/events/receiver.py
| Python | |
|---|---|
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |