ForcePilot/backend/package/yuxi/channels/adapters/feishu/accounts.py
Kris a6fa7245e5 feat(feishu): 完整实现飞书适配器核心模块
新增飞书机器人适配器全套功能,包括:
- 基础适配器入口与工具导出
- 消息格式化、卡片渲染、回复调度逻辑
- 会话ID生成、模型覆盖策略
- 消息发送缓存、顺序队列管理
- 飞书签名验证、加解密webhook请求
- 审批权限校验、机器人菜单事件处理
- 文档评论、钉消息、语音转码处理
- 静态/动态目录管理、子代理生命周期管理
- 各类工具集:聊天、云盘、文档、知识库API封装
2026-05-12 00:43:59 +08:00

135 lines
4.9 KiB
Python

from __future__ import annotations
import logging
import time
from dataclasses import dataclass, field
from typing import Any
logger = logging.getLogger(__name__)
@dataclass
class FeishuRateLimiter:
max_requests_per_minute: int = 100
_timestamps: list[float] = field(default_factory=list)
def acquire(self) -> bool:
now = time.monotonic()
window = now - 60
self._timestamps = [t for t in self._timestamps if t > window]
if len(self._timestamps) >= self.max_requests_per_minute:
return False
self._timestamps.append(now)
return True
def reset(self) -> None:
self._timestamps.clear()
@dataclass
class FeishuAccount:
name: str
app_id: str = ""
app_secret: str = ""
bot_open_id: str = ""
platform: str = "feishu"
domain: str = ""
encrypt_key: str = ""
verify_token: str = ""
enabled: bool = True
config: dict[str, Any] = field(default_factory=dict)
rate_limiter: FeishuRateLimiter = field(default_factory=FeishuRateLimiter)
class FeishuAccountManager:
def __init__(self, config: dict[str, Any] | None = None):
self._accounts: dict[str, FeishuAccount] = {}
self._default_account: str = ""
if config:
self._load_from_config(config)
def _load_from_config(self, config: dict[str, Any]) -> None:
accounts_cfg = config.get("accounts", {})
if not accounts_cfg:
main = FeishuAccount(
name="default",
app_id=config.get("app_id", ""),
app_secret=config.get("app_secret", ""),
bot_open_id=config.get("bot_open_id", ""),
platform=config.get("platform", "feishu"),
domain=config.get("domain", ""),
encrypt_key=config.get("encrypt_key", ""),
verify_token=config.get("verify_token", ""),
config=config,
)
self._accounts["default"] = main
self._default_account = "default"
return
self._default_account = config.get("defaultAccount", "")
for name, acct_cfg in accounts_cfg.items():
if not isinstance(acct_cfg, dict):
continue
merged = {**config, **acct_cfg}
rpm = acct_cfg.get("rateLimitPerMinute", acct_cfg.get("rate_limit_per_minute", 100))
account = FeishuAccount(
name=name,
app_id=acct_cfg.get("appId", acct_cfg.get("app_id", "")),
app_secret=acct_cfg.get("appSecret", acct_cfg.get("app_secret", "")),
bot_open_id=acct_cfg.get("botOpenId", acct_cfg.get("bot_open_id", "")),
platform=acct_cfg.get("platform", merged.get("platform", "feishu")),
domain=acct_cfg.get("domain", merged.get("domain", "")),
encrypt_key=acct_cfg.get("encryptKey", acct_cfg.get("encrypt_key", "")),
verify_token=acct_cfg.get("verifyToken", acct_cfg.get("verify_token", "")),
enabled=acct_cfg.get("enabled", True),
config=merged,
rate_limiter=FeishuRateLimiter(max_requests_per_minute=rpm if isinstance(rpm, int) else 100),
)
self._accounts[name] = account
if self._default_account not in self._accounts and self._accounts:
self._default_account = next(iter(self._accounts))
def get_account(self, name: str = "") -> FeishuAccount | None:
key = name or self._default_account
account = self._accounts.get(key)
if account and account.enabled:
return account
return None
def get_default_account(self) -> FeishuAccount | None:
return self.get_account(self._default_account)
def list_enabled(self) -> list[FeishuAccount]:
return [a for a in self._accounts.values() if a.enabled]
def list_all(self) -> list[FeishuAccount]:
return list(self._accounts.values())
def check_rate_limit(self, name: str = "") -> bool:
account = self.get_account(name)
if account is None:
return False
return account.rate_limiter.acquire()
def delete_account(self, name: str) -> bool:
if name == self._default_account:
logger.warning("[FeishuAccount] Cannot delete default account '%s'", name)
return False
if name not in self._accounts:
logger.warning("[FeishuAccount] Account '%s' not found", name)
return False
self._accounts.pop(name)
logger.info("[FeishuAccount] Deleted account '%s'", name)
return True
def get_account_status_line(self, name: str = "") -> str:
account = self.get_account(name) if name else self.get_default_account()
if account is None:
return "unknown: no account configured"
if account.enabled:
status = "active"
else:
status = "disabled"
return f"{account.name}: {status} (platform={account.platform})"