ForcePilot/backend/package/yuxi/channels/adapters/msteams/tool_policy.py
Kris 939f1ba82a refactor(msteams): 整理代码结构并新增多项功能
本次提交对Microsoft Teams适配器代码进行了多维度优化与新增:
1.  调整多处导入顺序,优化代码可读性
2.  新增media_tools工具模块,提供媒体相关辅助函数
3.  新增thread_history模块,实现对话历史拉取与缓存功能
4.  新增connection_modes模块,支持webhook/websocket/polling三种连接模式
5.  扩展security.py与tool_policy.py,新增通配符配置校验与三级策略解析
6.  新增feedback会话记录功能
7.  为sent_message_cache添加自动清理任务
8.  优化normalizer模块,新增引用、编辑消息解析与线程上下文注入
9.  重构file_upload的SSRF防护逻辑,复用公共校验工具
10. 修复多处导入顺序与代码排版问题
11. 为消息发送添加断路器保护与异步去重锁
2026-05-13 16:12:31 +08:00

150 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Microsoft Teams 工具策略 (Tools Policy)。
Team/Channel/Sender 三级 tools allow/deny 策略解析。
支持全局配置 + teams 嵌套覆盖 + toolsBySender 按发送者策略。
"""
from __future__ import annotations
from typing import Any
class ToolPolicy:
"""MSTeams 三级工具策略解析器。
优先级Sender > Channel > Team > Global deny > Global allow
"""
def __init__(self, config: dict[str, Any] | None = None):
config = config or {}
self._global_allow: set[str] = set(config.get("tools_allow", []))
self._global_deny: set[str] = set(config.get("tools_deny", []))
self._tools_by_sender: dict[str, dict[str, set[str]]] = {}
sender_tools = config.get("tools_by_sender", []) or []
if isinstance(sender_tools, dict):
for sender_id, tools in sender_tools.items():
if isinstance(tools, list):
self._tools_by_sender[sender_id] = {"allow": set(tools), "deny": set()}
elif isinstance(tools, dict):
self._tools_by_sender[sender_id] = {
"allow": set(tools.get("allow", [])),
"deny": set(tools.get("deny", [])),
}
elif isinstance(sender_tools, list):
for entry in sender_tools:
if isinstance(entry, dict):
sid = entry.get("sender_id", "")
if sid:
self._tools_by_sender[sid] = {
"allow": set(entry.get("allow", [])),
"deny": set(entry.get("deny", [])),
}
self._teams_config: dict[str, dict[str, Any]] = config.get("teams", {})
def is_tool_allowed(
self,
tool_name: str,
sender_id: str = "",
team_id: str = "",
channel_id: str = "",
) -> bool:
sender_rules = self._tools_by_sender.get(sender_id)
if sender_rules is not None:
if tool_name in sender_rules["deny"]:
return False
if sender_rules["allow"]:
return tool_name in sender_rules["allow"]
channel_allow, channel_deny = self._resolve_channel_tools(team_id, channel_id)
if tool_name in channel_deny:
return False
if channel_allow and tool_name not in channel_allow:
return False
team_allow, team_deny = self._resolve_team_tools(team_id)
if tool_name in team_deny:
return False
if team_allow and tool_name not in team_allow:
return False
if tool_name in self._global_deny:
return False
if self._global_allow:
return tool_name in self._global_allow
return True
def _resolve_team_tools(self, team_id: str) -> tuple[set[str], set[str]]:
team_config = self._teams_config.get(team_id) or self._teams_config.get("*") or {}
team_tools = team_config.get("tools", {})
allow = set(team_tools.get("allow", []))
deny = set(team_tools.get("deny", []))
return allow, deny
def _resolve_channel_tools(self, team_id: str, channel_id: str) -> tuple[set[str], set[str]]:
if not team_id or not channel_id:
return set(), set()
team_config = self._teams_config.get(team_id) or self._teams_config.get("*") or {}
channels_config = team_config.get("channels", {})
ch_config = channels_config.get(channel_id) or channels_config.get("*") or {}
ch_tools = ch_config.get("tools", {})
allow = set(ch_tools.get("allow", []))
deny = set(ch_tools.get("deny", []))
return allow, deny
def get_allowed_tools(
self,
sender_id: str = "",
team_id: str = "",
channel_id: str = "",
) -> list[str] | None:
sender_rules = self._tools_by_sender.get(sender_id)
if sender_rules is not None:
if sender_rules["allow"]:
return sorted(sender_rules["allow"] - sender_rules["deny"])
channel_allow, channel_deny = self._resolve_channel_tools(team_id, channel_id)
team_allow, team_deny = self._resolve_team_tools(team_id)
if channel_allow:
return sorted(channel_allow - channel_deny)
if team_allow:
return sorted(team_allow - team_deny)
if self._global_allow:
return sorted(self._global_allow - self._global_deny)
return None
def get_denied_tools(
self,
sender_id: str = "",
team_id: str = "",
channel_id: str = "",
) -> set[str]:
sender_rules = self._tools_by_sender.get(sender_id)
sender_deny = sender_rules["deny"] if sender_rules else set()
channel_allow, channel_deny = self._resolve_channel_tools(team_id, channel_id)
team_allow, team_deny = self._resolve_team_tools(team_id)
return sender_deny | channel_deny | team_deny | self._global_deny
def resolve_tool_policy(
config: dict[str, Any],
tool_name: str,
sender_id: str = "",
team_id: str = "",
channel_id: str = "",
) -> bool:
"""快捷函数:三级工具策略解析。
优先级Sender > Channel > Team > Global
"""
policy = ToolPolicy(config)
return policy.is_tool_allowed(tool_name, sender_id, team_id, channel_id)