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

143 lines
4.9 KiB
Python

from __future__ import annotations
import fnmatch
from dataclasses import dataclass, field
from typing import Any, Literal
from yuxi.channels.models import ChatType
@dataclass
class FeishuPolicy:
dm_policy: Literal["open", "pairing", "allowlist"] = "pairing"
group_policy: Literal["open", "allowlist", "disabled"] = "allowlist"
allowlist: list[str] = field(default_factory=list)
group_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
dm_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
_dm_prefixes = {"user:", "dm:", "open_id:"}
_group_prefixes = {"chat:", "group:", "channel:"}
def __post_init__(self):
self._dm_exact: set[str] = set()
self._dm_patterns: list[str] = []
self._group_exact: set[str] = set()
self._group_patterns: list[str] = []
self._has_wildcard = False
for entry in self.allowlist:
entry = entry.strip()
if not entry:
continue
if entry == "*":
self._has_wildcard = True
continue
if "*" in entry or "?" in entry:
self._group_patterns.append(entry)
continue
prefix, _, bare = _parse_prefix(entry)
if prefix in self._group_prefixes:
self._group_exact.add(bare)
elif prefix in self._dm_prefixes:
self._dm_exact.add(bare)
else:
self._group_exact.add(bare)
self._dm_exact.add(bare)
def check_dm_access(self, open_id: str) -> bool:
if self.dm_policy == "open":
return True
if self.dm_policy == "pairing":
return True
if self._has_wildcard:
return True
return open_id in self._dm_exact
def check_group_access(self, chat_id: str) -> bool:
if self.group_policy == "open":
return True
if self.group_policy == "disabled":
return False
if self._has_wildcard:
return True
if chat_id in self._group_exact:
return True
for pattern in self._group_patterns:
if fnmatch.fnmatch(chat_id, pattern):
return True
return False
def should_require_mention(self, chat_type: ChatType) -> bool:
if chat_type == ChatType.DIRECT:
return False
if self.group_policy == "open":
return False
return True
def get_group_override(self, chat_id: str) -> dict[str, Any]:
override = self.group_overrides.get(chat_id, {})
if override:
return override
for pattern, cfg in self.group_overrides.items():
if fnmatch.fnmatch(chat_id, pattern):
return cfg
return {}
def get_dm_override(self, open_id: str) -> dict[str, Any]:
return self.dm_overrides.get(open_id, {})
def get_effective_group_config(self, chat_id: str) -> dict[str, Any]:
override = self.get_group_override(chat_id)
if not override:
return {}
return {
"requireMention": override.get("requireMention"),
"systemPrompt": override.get("systemPrompt", ""),
"tools": override.get("tools", {}),
"skills": override.get("skills", {}),
"groupPolicy": override.get("groupPolicy", ""),
}
def get_effective_dm_config(self, open_id: str) -> dict[str, Any]:
override = self.get_dm_override(open_id)
if not override:
return {}
return {
"enabled": override.get("enabled", True),
"systemPrompt": override.get("systemPrompt", ""),
}
@classmethod
def from_config(cls, config: dict[str, Any]) -> FeishuPolicy:
group_overrides = {}
for entry in config.get("groupConfigOverrides", []):
chat_id = entry.get("chatId", entry.get("chat_id", ""))
if chat_id:
group_overrides[chat_id] = entry
dm_overrides = {}
for entry in config.get("dmConfigOverrides", []):
open_id = entry.get("openId", entry.get("open_id", ""))
if open_id:
dm_overrides[open_id] = entry
group_policy = config.get("group_policy", config.get("groupPolicy", "allowlist"))
if group_policy == "allowall":
group_policy = "open"
return cls(
dm_policy=config.get("dm_policy", config.get("dmPolicy", "pairing")),
group_policy=group_policy,
allowlist=config.get("allowlist", config.get("allowFrom", [])),
group_overrides=group_overrides,
dm_overrides=dm_overrides,
)
def _parse_prefix(entry: str) -> tuple[str, str, str]:
for sep in (":",):
if sep in entry:
prefix, bare = entry.split(sep, 1)
return f"{prefix}:", prefix, bare
return "", "", entry