ForcePilot/backend/package/yuxi/channel/extensions/slack/interactive.py
Kris bfc7755137 feat(channel): 添加 Slack 渠道扩展
新增 Slack 渠道扩展,支持在 Yuxi 平台中集成 Slack 团队协作平台。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- monitor: 渠道状态监控
- status: 会话状态管理
- actions: 交互动作处理
- interactive: 交互式消息
- commands: 斜杠指令
- threading: 线程管理
- mentions: @提及
- constants: 常量定义
- types: 类型定义
2026-05-21 11:43:23 +08:00

381 lines
14 KiB
Python

import re
import logging
from datetime import datetime, UTC
from yuxi.channel.extensions.slack.constants import SLACK_MAX_BUTTONS, SLACK_MAX_SELECT_OPTIONS
logger = logging.getLogger(__name__)
class SlackInteractive:
SLACK_BUTTONS_PATTERN = re.compile(r"\[\[slack_buttons:\s*(.+?)\]\]", re.DOTALL)
SLACK_SELECT_PATTERN = re.compile(r"\[\[slack_select:\s*(.+?)\]\]", re.DOTALL)
SLACK_DATEPICKER_PATTERN = re.compile(r"\[\[slack_datepicker:\s*(.+?)\]\]", re.DOTALL)
SLACK_USERS_PATTERN = re.compile(r"\[\[slack_users:\s*(.+?)\]\]", re.DOTALL)
SLACK_CHANNELS_PATTERN = re.compile(r"\[\[slack_channels:\s*(.+?)\]\]", re.DOTALL)
SLACK_RADIO_PATTERN = re.compile(r"\[\[slack_radio:\s*(.+?)\]\]", re.DOTALL)
SLACK_CHECKBOX_PATTERN = re.compile(r"\[\[slack_checkbox:\s*(.+?)\]\]", re.DOTALL)
def enable_interactive_replies(self, config: dict, account_id: str | None = None) -> bool:
return config.get("channels", {}).get("slack", {}).get("interactiveReplies", False)
def parse_buttons(self, text: str) -> list[dict] | None:
match = self.SLACK_BUTTONS_PATTERN.search(text)
if not match:
return None
raw = match.group(1).strip()
buttons = []
for item in raw.split(","):
item = item.strip()
if not item:
continue
parts = item.split(":")
if len(parts) >= 2:
label = parts[0].strip()
value = parts[1].strip()
style = parts[2].strip() if len(parts) > 2 else "default"
buttons.append({"text": label, "value": value, "style": style})
if len(buttons) >= SLACK_MAX_BUTTONS:
break
return buttons if buttons else None
def parse_select(self, text: str) -> dict | None:
match = self.SLACK_SELECT_PATTERN.search(text)
if not match:
return None
raw = match.group(1).strip()
parts = raw.split("|", 1)
placeholder = "选择一个选项"
options_str = parts[0].strip()
if len(parts) == 2:
placeholder = parts[0].strip()
options_str = parts[1].strip()
options = []
for item in options_str.split(","):
item = item.strip()
if not item:
continue
kv = item.split(":", 1)
if len(kv) == 2:
options.append({"text": kv[0].strip(), "value": kv[1].strip()})
if len(options) >= SLACK_MAX_SELECT_OPTIONS:
break
if not options:
return None
return {"placeholder": placeholder, "options": options}
def detect_auto_select(self, text: str) -> dict | None:
lines = text.strip().split("\n")
last_line = lines[-1].strip() if lines else ""
if last_line.lower().startswith("options:"):
options_text = last_line[len("options:") :].strip()
options = []
for item in options_text.split(","):
item = item.strip()
if not item:
continue
kv = item.split(":", 1)
if len(kv) == 2:
options.append({"text": kv[0].strip(), "value": kv[1].strip()})
if not options:
return None
if len(options) <= SLACK_MAX_BUTTONS:
buttons = [{"text": o["text"], "value": o["value"], "style": "default"} for o in options]
return {"type": "buttons", "buttons": buttons}
return {"type": "select", "placeholder": "选择一个选项", "options": options}
return None
def build_block_kit_buttons(self, buttons: list[dict]) -> list[dict]:
block_elements = []
for btn in buttons:
style = btn.get("style", "default")
element = {
"type": "button",
"text": {"type": "plain_text", "text": btn["text"]},
"value": btn["value"],
"action_id": f"slack_btn_{btn['value'][:20]}",
}
if style in ("primary", "danger"):
element["style"] = style
block_elements.append(element)
return [{"type": "actions", "elements": block_elements}]
def build_block_kit_select(self, select_data: dict) -> list[dict]:
options = []
for opt in select_data["options"]:
options.append(
{
"text": {"type": "plain_text", "text": opt["text"]},
"value": opt["value"],
}
)
return [
{
"type": "section",
"accessory": {
"type": "static_select",
"placeholder": {"type": "plain_text", "text": select_data["placeholder"]},
"options": options,
"action_id": "slack_select_default",
},
}
]
def build_datepicker(self, action_id: str, placeholder: str = "选择日期") -> dict:
return {
"type": "section",
"accessory": {
"type": "datepicker",
"initial_date": datetime.now().strftime("%Y-%m-%d"),
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
def build_overflow_menu(self, options: list[dict], action_id: str) -> dict:
return {
"type": "actions",
"elements": [
{
"type": "overflow",
"options": [
{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options[:5]
],
"action_id": action_id,
}
],
}
def build_users_select(self, action_id: str, placeholder: str = "选择用户") -> dict:
return {
"type": "section",
"accessory": {
"type": "users_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
def build_conversations_select(self, action_id: str, placeholder: str = "选择对话") -> dict:
return {
"type": "section",
"accessory": {
"type": "conversations_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
def build_channels_select(self, action_id: str, placeholder: str = "选择频道") -> dict:
return {
"type": "section",
"accessory": {
"type": "channels_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
def build_radio_buttons(self, options: list[dict], action_id: str) -> dict:
return {
"type": "section",
"accessory": {
"type": "radio_buttons",
"options": [{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options],
"action_id": action_id,
},
}
def build_checkboxes(self, options: list[dict], action_id: str) -> dict:
return {
"type": "section",
"accessory": {
"type": "checkboxes",
"options": [{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options],
"action_id": action_id,
},
}
def build_timepicker(self, action_id: str, placeholder: str = "选择时间", initial_time: str | None = None) -> dict:
element = {
"type": "section",
"accessory": {
"type": "timepicker",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
if initial_time:
element["accessory"]["initial_time"] = initial_time
return element
def build_datetimepicker(self, action_id: str, placeholder: str = "选择日期时间") -> dict:
from datetime import datetime
now = datetime.now(UTC)
return {
"type": "section",
"accessory": {
"type": "datetimepicker",
"initial_date_time": int(now.timestamp()),
"action_id": action_id,
},
}
def build_plain_text_input(
self, action_id: str, placeholder: str = "输入文本", multiline: bool = False, max_length: int | None = None
) -> dict:
element = {
"type": "input",
"element": {
"type": "plain_text_input",
"action_id": action_id,
"placeholder": {"type": "plain_text", "text": placeholder},
"multiline": multiline,
},
"label": {"type": "plain_text", "text": placeholder},
}
if max_length:
element["element"]["max_length"] = max_length
return element
def build_number_input(
self, action_id: str, placeholder: str = "输入数字", is_decimal_allowed: bool = False
) -> dict:
return {
"type": "input",
"element": {
"type": "number_input",
"is_decimal_allowed": is_decimal_allowed,
"action_id": action_id,
"placeholder": {"type": "plain_text", "text": placeholder},
},
"label": {"type": "plain_text", "text": placeholder},
}
def build_multi_static_select(
self,
options: list[dict],
action_id: str,
placeholder: str = "选择多个选项",
max_selected_items: int | None = None,
) -> dict:
element = {
"type": "section",
"accessory": {
"type": "multi_static_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"options": [{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options],
"action_id": action_id,
},
}
if max_selected_items:
element["accessory"]["max_selected_items"] = max_selected_items
return element
def build_multi_users_select(
self, action_id: str, placeholder: str = "选择多个用户", max_selected_items: int | None = None
) -> dict:
element = {
"type": "section",
"accessory": {
"type": "multi_users_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
if max_selected_items:
element["accessory"]["max_selected_items"] = max_selected_items
return element
def build_multi_channels_select(
self, action_id: str, placeholder: str = "选择多个频道", max_selected_items: int | None = None
) -> dict:
element = {
"type": "section",
"accessory": {
"type": "multi_channels_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
},
}
if max_selected_items:
element["accessory"]["max_selected_items"] = max_selected_items
return element
def build_external_select(self, action_id: str, placeholder: str = "搜索选项", min_query_length: int = 3) -> dict:
return {
"type": "section",
"accessory": {
"type": "external_select",
"placeholder": {"type": "plain_text", "text": placeholder},
"action_id": action_id,
"min_query_length": min_query_length,
},
}
def parse_datepicker(self, text: str) -> dict | None:
match = self.SLACK_DATEPICKER_PATTERN.search(text)
if not match:
return None
placeholder = match.group(1).strip()
return {"placeholder": placeholder or "选择日期", "action_id": "slack_datepicker_default"}
def parse_users_select(self, text: str) -> dict | None:
match = self.SLACK_USERS_PATTERN.search(text)
if not match:
return None
placeholder = match.group(1).strip()
return {"placeholder": placeholder or "选择用户", "action_id": "slack_users_default"}
def parse_channels_select(self, text: str) -> dict | None:
match = self.SLACK_CHANNELS_PATTERN.search(text)
if not match:
return None
placeholder = match.group(1).strip()
return {"placeholder": placeholder or "选择频道", "action_id": "slack_channels_default"}
def parse_radio_buttons(self, text: str) -> list[dict] | None:
match = self.SLACK_RADIO_PATTERN.search(text)
if not match:
return None
raw = match.group(1).strip()
options = []
for item in raw.split(","):
item = item.strip()
if not item:
continue
parts = item.split(":", 1)
if len(parts) == 2:
options.append({"text": parts[0].strip(), "value": parts[1].strip()})
return options if options else None
def parse_checkboxes(self, text: str) -> list[dict] | None:
match = self.SLACK_CHECKBOX_PATTERN.search(text)
if not match:
return None
raw = match.group(1).strip()
options = []
for item in raw.split(","):
item = item.strip()
if not item:
continue
parts = item.split(":", 1)
if len(parts) == 2:
options.append({"text": parts[0].strip(), "value": parts[1].strip()})
return options if options else None