新增 Slack 渠道扩展,支持在 Yuxi 平台中集成 Slack 团队协作平台。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - monitor: 渠道状态监控 - status: 会话状态管理 - actions: 交互动作处理 - interactive: 交互式消息 - commands: 斜杠指令 - threading: 线程管理 - mentions: @提及 - constants: 常量定义 - types: 类型定义
381 lines
14 KiB
Python
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
|