- 新增钉钉工具调用审计日志功能 - 新增参数校验装饰器与基础工具集(文档、审批、多维表格) - 完善事件类型映射与会话ID生成逻辑 - 新增消息去重、访问策略匹配、配置校验与UI提示 - 新增酷应用卡片、目录管理、表情反应支持 - 优化消息发送逻辑,添加401重试机制 - 新增配置化的权限控制与流式输出支持
127 lines
5.2 KiB
Python
127 lines
5.2 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 DingDingPolicyMatcher:
|
|
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._global_system_prompt = config.get("systemPrompt", config.get("system_prompt", ""))
|
|
self._groups_config: dict[str, dict[str, Any]] = config.get("groups", {})
|
|
self._dms_config: dict[str, dict[str, Any]] = config.get("dms", {})
|
|
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
|
|
|
|
if policy == "pairing":
|
|
return True, ""
|
|
|
|
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 resolve_dm_config(self, chat_id: str) -> dict[str, Any]:
|
|
return self._dms_config.get(chat_id, {})
|
|
|
|
def resolve_system_prompt(self, chat_id: str, chat_type: str) -> str:
|
|
if chat_type == "direct":
|
|
dm_cfg = self._dms_config.get(chat_id, {})
|
|
return dm_cfg.get("systemPrompt", dm_cfg.get("system_prompt", self._global_system_prompt))
|
|
group_cfg = self._groups_config.get(chat_id, {})
|
|
return group_cfg.get("systemPrompt", group_cfg.get("system_prompt", self._global_system_prompt))
|
|
|
|
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", "pairing"):
|
|
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
|