from __future__ import annotations import re from typing import Any from yuxi.channels.adapters.slack.blocks import ( build_button, build_actions, build_static_select, build_option, ) _MAX_BUTTONS = 5 _MAX_SELECT_OPTIONS = 100 _AUTO_SELECT_THRESHOLD = 5 _AUTO_SELECT_MAX_ITEMS = 12 _BUTTON_PATTERN = re.compile( r"\[\[slack_buttons:\s*(.+?):\s*(.+?)(?::\s*(primary|danger))?\s*\]\]", re.IGNORECASE, ) _SELECT_PATTERN = re.compile( r"\[\[slack_select:\s*(.+?)\s*\|\s*(.+?):\s*(.+?)\s*\]\]", re.IGNORECASE, ) _AUTO_SELECT_LINE = re.compile(r"^\s*Options:\s*$", re.IGNORECASE) _OPTION_LINE = re.compile(r"^\s*-\s*(.+?):\s*(.+?)\s*$") def compile_interactive_replies(text: str) -> tuple[str, list[dict[str, Any]] | None]: blocks = _compile_buttons(text) if blocks: cleaned = _BUTTON_PATTERN.sub("", text).strip() return cleaned, blocks blocks = _compile_select(text) if blocks: cleaned = _SELECT_PATTERN.sub("", text).strip() return cleaned, blocks cleaned, auto_blocks = _compile_auto_select(text) if auto_blocks: return cleaned, auto_blocks return text, None def _compile_buttons(text: str) -> list[dict[str, Any]] | None: matches = _BUTTON_PATTERN.findall(text) if not matches: return None buttons = [] for label, value, style in matches[:_MAX_BUTTONS]: btn_style = style.lower().strip() if style else "" action_id = f"slack_button_{value.strip().replace(' ', '_')}" btn = build_button( label.strip(), action_id, value=value.strip(), style=btn_style if btn_style in ("primary", "danger") else "", ) buttons.append(btn) return [build_actions(*buttons)] if buttons else None def _compile_select(text: str) -> list[dict[str, Any]] | None: matches = _SELECT_PATTERN.findall(text) if not matches: return None options = [] for placeholder, label, value in matches[:_MAX_SELECT_OPTIONS]: options.append(build_option(label.strip(), value.strip())) if not options: return None placeholder_text = matches[0][0].strip() if matches else "选择一个选项" select = build_static_select( "slack_select_menu", *options, placeholder=placeholder_text, ) return [{"type": "section", "text": {"type": "mrkdwn", "text": "请选择:"}, "accessory": select}] def _compile_auto_select(text: str) -> tuple[str, list[dict[str, Any]] | None]: lines = text.split("\n") options_start = -1 for i, line in enumerate(lines): if _AUTO_SELECT_LINE.match(line): options_start = i break if options_start < 0: return text, None option_lines = [] for line in lines[options_start + 1 :]: match = _OPTION_LINE.match(line) if match: if len(option_lines) >= _AUTO_SELECT_MAX_ITEMS: break option_lines.append((match.group(1).strip(), match.group(2).strip())) elif option_lines and not line.strip(): break elif not line.strip(): continue else: break if not option_lines: return text, None cleaned_lines = lines[:options_start] cleaned = "\n".join(cleaned_lines).strip() if len(option_lines) <= _AUTO_SELECT_THRESHOLD: buttons = [ build_button(label, f"auto_button_{value.replace(' ', '_')}", value=value) for label, value in option_lines ] blocks = [build_actions(*buttons)] return cleaned, blocks select_options = [build_option(label, value) for label, value in option_lines] select = build_static_select("auto_select_menu", *select_options, placeholder="选择一个选项") blocks = [{"type": "section", "text": {"type": "mrkdwn", "text": "请选择:"}, "accessory": select}] return cleaned, blocks def has_interactive_syntax(text: str) -> bool: return bool(_BUTTON_PATTERN.search(text) or _SELECT_PATTERN.search(text) or _AUTO_SELECT_LINE.search(text))