新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能: 1. 新增语音、视觉相关的TTS和图像分析导出接口 2. 实现消息预处理、路由、线程上下文处理的完整流水线 3. 新增账号管理、缓存机制、房间上下文提取功能 4. 支持Webhook和Socket Mode两种事件接收方式 5. 实现权限白名单、审批配对、自动状态管理功能 6. 新增配置迁移、作用域校验、重连策略等辅助模块
132 lines
3.9 KiB
Python
132 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Awaitable
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
@dataclass
|
|
class InteractionPayload:
|
|
type: str = ""
|
|
action_id: str = ""
|
|
user: str = ""
|
|
channel: str = ""
|
|
ts: str = ""
|
|
thread_ts: str = ""
|
|
value: str = ""
|
|
raw: dict[str, Any] | None = None
|
|
|
|
@property
|
|
def is_button(self) -> bool:
|
|
return self.type == "button"
|
|
|
|
@property
|
|
def is_select(self) -> bool:
|
|
return self.type == "static_select"
|
|
|
|
@property
|
|
def is_overflow(self) -> bool:
|
|
return self.type == "overflow"
|
|
|
|
|
|
@dataclass
|
|
class InteractionRoute:
|
|
action_id: str
|
|
handler: Callable[[InteractionPayload], Awaitable[dict[str, Any]]]
|
|
description: str = ""
|
|
|
|
async def execute(self, payload: InteractionPayload) -> dict[str, Any]:
|
|
try:
|
|
return await self.handler(payload)
|
|
except Exception as e:
|
|
logger.error(f"Interaction route '{self.action_id}' failed: {e}")
|
|
return {"error": str(e)}
|
|
|
|
|
|
@dataclass
|
|
class InteractionRouter:
|
|
routes: dict[str, InteractionRoute] = field(default_factory=dict)
|
|
|
|
def register(self, action_id: str, handler: Callable, description: str = "") -> None:
|
|
self.routes[action_id] = InteractionRoute(
|
|
action_id=action_id,
|
|
handler=handler,
|
|
description=description,
|
|
)
|
|
|
|
def unregister(self, action_id: str) -> None:
|
|
self.routes.pop(action_id, None)
|
|
|
|
def get_route(self, action_id: str) -> InteractionRoute | None:
|
|
if action_id in self.routes:
|
|
return self.routes[action_id]
|
|
for prefix, route in self.routes.items():
|
|
if prefix.endswith("*") and action_id.startswith(prefix[:-1]):
|
|
return route
|
|
return None
|
|
|
|
async def dispatch(self, payload: InteractionPayload) -> dict[str, Any]:
|
|
route = self.get_route(payload.action_id)
|
|
if route:
|
|
return await route.execute(payload)
|
|
logger.debug(f"No route found for action_id: {payload.action_id}")
|
|
return {"acknowledged": True, "routed": False}
|
|
|
|
@property
|
|
def registered_actions(self) -> list[str]:
|
|
return list(self.routes.keys())
|
|
|
|
|
|
async def dispatch_interaction(
|
|
client,
|
|
payload: InteractionPayload,
|
|
*,
|
|
handlers: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
if not handlers:
|
|
handlers = {}
|
|
|
|
handler = handlers.get(payload.action_id)
|
|
if handler:
|
|
try:
|
|
return await handler(payload)
|
|
except Exception as e:
|
|
logger.error(f"Interaction handler error for {payload.action_id}: {e}")
|
|
return {"error": str(e)}
|
|
|
|
logger.debug(f"No handler found for action_id: {payload.action_id}")
|
|
return {"acknowledged": True}
|
|
|
|
|
|
def parse_interaction_payload(raw_payload: dict[str, Any]) -> InteractionPayload:
|
|
actions = raw_payload.get("actions", [])
|
|
action = actions[0] if actions else {}
|
|
|
|
return InteractionPayload(
|
|
type=raw_payload.get("type", action.get("type", "")),
|
|
action_id=action.get("action_id", ""),
|
|
user=raw_payload.get("user", {}).get("id", ""),
|
|
channel=raw_payload.get("channel", {}).get("id", ""),
|
|
ts=raw_payload.get("message", {}).get("ts", ""),
|
|
thread_ts=raw_payload.get("message", {}).get("thread_ts", ""),
|
|
value=action.get("value", action.get("selected_option", {}).get("value", "")),
|
|
raw=raw_payload,
|
|
)
|
|
|
|
|
|
_external_menu_store: dict[str, list[dict[str, Any]]] = {}
|
|
|
|
|
|
def store_external_options(request_id: str, options: list[dict[str, Any]]) -> None:
|
|
_external_menu_store[request_id] = options
|
|
|
|
|
|
def get_external_options(request_id: str) -> list[dict[str, Any]]:
|
|
return _external_menu_store.pop(request_id, [])
|
|
|
|
|
|
def clear_external_options() -> None:
|
|
_external_menu_store.clear()
|