新增 Microsoft Teams 渠道扩展,支持在 Yuxi 平台中集成 Microsoft Teams 协作平台。 包含以下功能模块: - sdk: Bot Framework SDK 封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - auth: JWT 认证 - jwks: JWKS 密钥管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - state: 状态管理 - runtime: 运行时管理 - actions: 动作处理 - adaptive_card: 自适应卡片 - task_modules: 任务模块 - message_extension: 消息扩展 - proactive: Proactive Messaging - graph: Microsoft Graph API 集成 - graph_teams: Teams 操作 - graph_members: 成员管理 - graph_messages: 消息获取 - graph_thread: 线程管理 - graph_users: 用户管理 - graph_upload: 文件上传 - files: 文件处理 - file_consent: 文件授权 - conversations: 会话存储 - mentions: @提及处理 - threading: 线程管理 - reactions: 表情反应 - polls: 投票功能 - meetings: 会议集成 - feedback: 反馈处理 - sso: 单点登录 - deep_links: 深层链接 - incoming_webhook: 入站 Webhook - localization: 本地化 - user_agent: 用户代理 - sent_message_cache: 消息缓存 - types: 类型定义
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import UTC, datetime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
THREAD_PARENT_CACHE_MAX = 100
|
|
THREAD_PARENT_CACHE_TTL_SECONDS = 300
|
|
|
|
|
|
class ThreadSessionManager:
|
|
def __init__(self):
|
|
self._parent_cache: dict[str, tuple[datetime, dict]] = {}
|
|
|
|
def cache_thread_parent(self, thread_id: str, parent_msg: dict) -> None:
|
|
now = datetime.now(UTC)
|
|
self._parent_cache[thread_id] = (now, parent_msg)
|
|
self._evict_expired()
|
|
|
|
def get_thread_parent(self, thread_id: str) -> dict | None:
|
|
now = datetime.now(UTC)
|
|
entry = self._parent_cache.get(thread_id)
|
|
if not entry:
|
|
return None
|
|
cached_at, parent = entry
|
|
if (now - cached_at).total_seconds() > THREAD_PARENT_CACHE_TTL_SECONDS:
|
|
self._parent_cache.pop(thread_id, None)
|
|
return None
|
|
return parent
|
|
|
|
def _evict_expired(self) -> None:
|
|
if len(self._parent_cache) <= THREAD_PARENT_CACHE_MAX:
|
|
return
|
|
now = datetime.now(UTC)
|
|
expired = [
|
|
k for k, (t, _) in self._parent_cache.items() if (now - t).total_seconds() > THREAD_PARENT_CACHE_TTL_SECONDS
|
|
]
|
|
for k in expired:
|
|
self._parent_cache.pop(k, None)
|
|
|
|
if len(self._parent_cache) > THREAD_PARENT_CACHE_MAX:
|
|
sorted_keys = sorted(
|
|
self._parent_cache.keys(),
|
|
key=lambda k: self._parent_cache[k][0],
|
|
)
|
|
for k in sorted_keys[: len(self._parent_cache) - THREAD_PARENT_CACHE_MAX]:
|
|
self._parent_cache.pop(k, None)
|
|
|
|
|
|
def resolve_msteams_session_key(
|
|
conversation_id: str,
|
|
*,
|
|
is_channel_thread: bool = False,
|
|
thread_root_message_id: str | None = None,
|
|
) -> str:
|
|
if is_channel_thread and thread_root_message_id:
|
|
return f"msteams:thread:{conversation_id}:{thread_root_message_id}"
|
|
return f"msteams:{conversation_id}"
|
|
|
|
|
|
def resolve_msteams_thread_id(
|
|
reply_to_id: str | None,
|
|
conversation_type: str | None,
|
|
) -> str | None:
|
|
if conversation_type == "channel" and reply_to_id:
|
|
return reply_to_id
|
|
return None
|
|
|
|
|
|
def format_thread_history(messages: list[dict]) -> str:
|
|
if not messages:
|
|
return ""
|
|
|
|
lines = ["[Thread history]"]
|
|
for msg in reversed(messages):
|
|
ts = msg.get("timestamp", "")
|
|
sender = msg.get("from_name", msg.get("from", {}).get("name", "Unknown"))
|
|
text = msg.get("text", "")
|
|
ts_display = ts[:19].replace("T", " ") if ts else ""
|
|
if ts_display:
|
|
lines.append(f"{ts_display} {sender}: {text}")
|
|
else:
|
|
lines.append(f"{sender}: {text}")
|
|
lines.append("[/Thread history]")
|
|
return "\n".join(lines)
|