ForcePilot/backend/package/yuxi/channels/adapters/feishu/accounts.py
Kris 69b7139863 refactor(feishu): 清理冗余代码并补全飞书工具集实现
1.  移除多个文件中的空行、冗余导入
2.  修复文件末尾缺少换行符的问题
3.  新增并补全飞书多类工具API实现:
    - 多维表格:更新、删除记录,列出视图
    - 文档:更新、追加、删除块
    - 云文档:上传、下载文件
    - 群组:创建、添加成员、更新信息、创建公告
    - 目录:重构用户部门缓存逻辑
4.  优化消息发送、回复、转发等API的错误处理和逻辑
5.  新增消息列表查询、已读状态查询等功能
2026-05-13 16:07:59 +08:00

191 lines
7.4 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 FeishuAccountPermission:
allowed_chat_ids: set[str] = field(default_factory=set)
allowed_user_ids: set[str] = field(default_factory=set)
allowed_tool_scopes: set[str] = field(default_factory=set)
is_admin: bool = False
allow_all: bool = False
def can_access_chat(self, chat_id: str) -> bool:
if self.allow_all or self.is_admin:
return True
return chat_id in self.allowed_chat_ids
def can_access_user(self, user_id: str) -> bool:
if self.allow_all or self.is_admin:
return True
return user_id in self.allowed_user_ids
def can_use_tool(self, tool_name: str) -> bool:
if self.allow_all or self.is_admin:
return True
if not self.allowed_tool_scopes:
return False
return tool_name in self.allowed_tool_scopes
@classmethod
def from_config(cls, perm_cfg: dict[str, Any] | None) -> FeishuAccountPermission:
if not perm_cfg:
return cls()
return cls(
allowed_chat_ids=set(perm_cfg.get("allowedChatIds", perm_cfg.get("allowed_chat_ids", []))),
allowed_user_ids=set(perm_cfg.get("allowedUserIds", perm_cfg.get("allowed_user_ids", []))),
allowed_tool_scopes=set(perm_cfg.get("allowedToolScopes", perm_cfg.get("allowed_tool_scopes", []))),
is_admin=perm_cfg.get("isAdmin", perm_cfg.get("is_admin", False)),
allow_all=perm_cfg.get("allowAll", perm_cfg.get("allow_all", False)),
)
@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)
permissions: FeishuAccountPermission = field(default_factory=FeishuAccountPermission)
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))
perm_cfg = acct_cfg.get("permissions", acct_cfg.get("permission", {}))
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),
permissions=FeishuAccountPermission.from_config(perm_cfg if isinstance(perm_cfg, dict) else {}),
)
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 check_chat_access(self, chat_id: str, account_name: str = "") -> bool:
account = self.get_account(account_name)
if account is None:
return False
return account.permissions.can_access_chat(chat_id)
def check_user_access(self, user_id: str, account_name: str = "") -> bool:
account = self.get_account(account_name)
if account is None:
return False
return account.permissions.can_access_user(user_id)
def check_tool_access(self, tool_name: str, account_name: str = "") -> bool:
account = self.get_account(account_name)
if account is None:
return False
return account.permissions.can_use_tool(tool_name)
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"
status = "active" if account.enabled else "disabled"
return f"{account.name}: {status} (platform={account.platform})"