ForcePilot/backend/package/yuxi/channel/extensions/msteams/threading.py
Kris 94444ced96 feat(channel): 添加 Microsoft Teams 渠道扩展
新增 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: 类型定义
2026-05-21 11:28:42 +08:00

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)