ForcePilot/backend/package/yuxi/channels/adapters/slack/interaction_dispatch.py
Kris a2aa782b86 feat(slack adapter): 实现完整的Slack频道适配器基础功能
新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能:
1. 新增语音、视觉相关的TTS和图像分析导出接口
2. 实现消息预处理、路由、线程上下文处理的完整流水线
3. 新增账号管理、缓存机制、房间上下文提取功能
4. 支持Webhook和Socket Mode两种事件接收方式
5. 实现权限白名单、审批配对、自动状态管理功能
6. 新增配置迁移、作用域校验、重连策略等辅助模块
2026-05-12 00:48:57 +08:00

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()