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

112 lines
4.5 KiB
Python

from __future__ import annotations
import re
import time
from typing import Any
_WILDCARD_PATTERN = re.compile(r"[*?\[\]]")
class PolicyAccessTracker:
MAX_HISTORY_PER_KEY = 100
def __init__(self):
self._access_log: dict[str, list[tuple[float, str, bool]]] = {}
def record_access(self, key: str, resource_id: str, allowed: bool) -> None:
if key not in self._access_log:
self._access_log[key] = []
entry = (time.monotonic(), resource_id, allowed)
self._access_log[key].append(entry)
if len(self._access_log[key]) > self.MAX_HISTORY_PER_KEY:
self._access_log[key] = self._access_log[key][-self.MAX_HISTORY_PER_KEY :]
def get_history(self, key: str) -> list[dict[str, Any]]:
entries = self._access_log.get(key, [])
return [{"timestamp": ts, "resource_id": rid, "allowed": allowed} for ts, rid, allowed in entries]
def count_recent(self, key: str, window_s: float = 60) -> tuple[int, int]:
entries = self._access_log.get(key, [])
now = time.monotonic()
allowed = sum(1 for ts, _, ok in entries if now - ts <= window_s and ok)
denied = sum(1 for ts, _, ok in entries if now - ts <= window_s and not ok)
return allowed, denied
def clear(self, key: str = "") -> None:
if key:
self._access_log.pop(key, None)
else:
self._access_log.clear()
class FeishuPolicyMatcher:
def __init__(self, config: dict[str, Any]):
self._group_policy = config.get("groupPolicy", config.get("group_policy", "allowlist"))
self._dm_policy = config.get("dmPolicy", config.get("dm_policy", "pairing"))
self._allowlist = set(config.get("allowFrom", config.get("allowlist", [])))
self._blocklist = set(config.get("blockFrom", config.get("blocklist", [])))
self._deny_message = config.get("denyMessage", "对不起,您没有权限使用此机器人。")
self._global_require_mention = config.get("requireMention", True)
self._groups_config: dict[str, dict[str, Any]] = config.get("groups", {})
self._tracker = PolicyAccessTracker()
def check_chat_access(self, chat_id: str, chat_type: str) -> tuple[bool, str]:
if self._blocklist:
if any(self._match_pattern(chat_id, pattern) for pattern in self._blocklist):
self._tracker.record_access("blocklist", chat_id, False)
return False, self._deny_message
policy = self._dm_policy if chat_type == "direct" else self._group_policy
if policy == "open":
self._tracker.record_access(policy, chat_id, True)
return True, ""
if policy == "allowlist":
if not self._allowlist:
self._tracker.record_access(policy, chat_id, False)
return False, self._deny_message
matched = any(self._match_pattern(chat_id, pattern) for pattern in self._allowlist)
self._tracker.record_access(policy, chat_id, matched)
if matched:
return True, ""
return False, self._deny_message
if policy == "disabled":
self._tracker.record_access(policy, chat_id, False)
return False, self._deny_message
self._tracker.record_access(policy, chat_id, False)
return False, self._deny_message
def resolve_require_mention(self, chat_id: str) -> bool:
group_cfg = self._groups_config.get(chat_id, {})
if "requireMention" in group_cfg:
return bool(group_cfg["requireMention"])
return self._global_require_mention
def resolve_group_config(self, chat_id: str) -> dict[str, Any]:
return self._groups_config.get(chat_id, {})
def get_access_history(self, policy_type: str = "") -> list[dict[str, Any]]:
if policy_type:
return self._tracker.get_history(policy_type)
result = []
for key in ("open", "allowlist", "disabled", "blocklist"):
result.extend(self._tracker.get_history(key))
return sorted(result, key=lambda x: x.get("timestamp", 0))
def reset_tracker(self) -> None:
self._tracker.clear()
@staticmethod
def _match_pattern(value: str, pattern: str) -> bool:
if value == pattern:
return True
if _WILDCARD_PATTERN.search(pattern):
try:
return bool(re.fullmatch(pattern.replace("*", ".*").replace("?", "."), value))
except re.error:
return False
return False