新增飞书机器人适配器全套功能,包括: - 基础适配器入口与工具导出 - 消息格式化、卡片渲染、回复调度逻辑 - 会话ID生成、模型覆盖策略 - 消息发送缓存、顺序队列管理 - 飞书签名验证、加解密webhook请求 - 审批权限校验、机器人菜单事件处理 - 文档评论、钉消息、语音转码处理 - 静态/动态目录管理、子代理生命周期管理 - 各类工具集:聊天、云盘、文档、知识库API封装
89 lines
2.5 KiB
Python
89 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
WEBHOOK_MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
WEBHOOK_RATE_WINDOW_S = 60
|
|
WEBHOOK_RATE_MAX_REQUESTS = 1000
|
|
|
|
_rate_counters: defaultdict[str, list[float]] = defaultdict(list)
|
|
|
|
|
|
def verify_feishu_signature(headers: dict, body: bytes, encrypt_key: str = "") -> bool:
|
|
if len(body) > WEBHOOK_MAX_BODY_BYTES:
|
|
return False
|
|
|
|
timestamp = headers.get("X-Lark-Request-Timestamp", "")
|
|
nonce = headers.get("X-Lark-Request-Nonce", "")
|
|
signature = headers.get("X-Lark-Signature", "")
|
|
|
|
if not timestamp or not nonce or not signature:
|
|
return False
|
|
|
|
raw = f"{timestamp}{nonce}{encrypt_key}".encode() + body
|
|
expected = hashlib.sha256(raw).hexdigest()
|
|
|
|
return signature == expected
|
|
|
|
|
|
def check_webhook_rate_limit(source_ip: str = "default") -> bool:
|
|
now = time.monotonic()
|
|
window = _rate_counters[source_ip]
|
|
window[:] = [t for t in window if now - t < WEBHOOK_RATE_WINDOW_S]
|
|
if len(window) >= WEBHOOK_RATE_MAX_REQUESTS:
|
|
return False
|
|
window.append(now)
|
|
return True
|
|
|
|
|
|
def decrypt_feishu_body(encrypted_body: bytes, encrypt_key: str) -> bytes | None:
|
|
if not encrypt_key:
|
|
return None
|
|
|
|
import base64
|
|
|
|
try:
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Util.Padding import unpad
|
|
except ImportError:
|
|
try:
|
|
from Cryptodome.Cipher import AES
|
|
from Cryptodome.Util.Padding import unpad
|
|
except ImportError:
|
|
return None
|
|
|
|
try:
|
|
key = hashlib.sha256(encrypt_key.encode()).digest()
|
|
raw = base64.b64decode(encrypted_body)
|
|
iv = raw[:16]
|
|
ciphertext = raw[16:]
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
|
return plaintext
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def verify_and_decrypt_webhook(
|
|
headers: dict,
|
|
body: bytes,
|
|
encrypt_key: str = "",
|
|
source_ip: str = "default",
|
|
) -> tuple[bool, bytes | None, str]:
|
|
if not check_webhook_rate_limit(source_ip):
|
|
return False, None, "rate_limited"
|
|
|
|
if not verify_feishu_signature(headers, body, encrypt_key):
|
|
return False, None, "signature_mismatch"
|
|
|
|
encrypted = headers.get("X-Lark-Request-Encrypted", "")
|
|
if encrypted and encrypt_key:
|
|
decrypted = decrypt_feishu_body(body, encrypt_key)
|
|
if decrypted is None:
|
|
return False, None, "decrypt_failed"
|
|
return True, decrypted, "ok"
|
|
|
|
return True, body, "ok"
|