from __future__ import annotations from typing import Any TEXT_CHUNK_LIMIT = 30000 CARD_CONTENT_LIMIT = 30000 CARD_TEMPLATE_COLORS = { "blue", "green", "red", "orange", "purple", "indigo", "wathet", "turquoise", "yellow", "grey", "carmine", "violet", "lime", } PRESENTATION_TONE_MAP = { "default": "blue", "danger": "red", "warning": "orange", "success": "green", } BUTTON_TYPE_MAP = { "primary": "primary", "danger": "danger", "default": "default", "": "default", } SELECTOR_TAGS = { "select_static", "select_person", "date_picker", "time_picker", "datetime_picker", "overflow", } def resolve_template_color(template: str | None = None, tone: str | None = None) -> str: if template and template in CARD_TEMPLATE_COLORS: return template if tone and tone in PRESENTATION_TONE_MAP: return PRESENTATION_TONE_MAP[tone] return "blue" def resolve_button_type(style: str | None = None) -> str: if not style: return "default" return BUTTON_TYPE_MAP.get(style, "default") def resolve_selector_tag(tag: str) -> str: return tag if tag in SELECTOR_TAGS else "select_static" def _build_plain_text(content: str) -> dict[str, Any]: return {"tag": "plain_text", "content": content} def _build_options(options: list[dict[str, str]]) -> list[dict[str, Any]]: return [{"text": _build_plain_text(opt.get("text", "")), "value": opt.get("value", "")} for opt in options] def make_select_static( placeholder: str = "", options: list[dict[str, str]] | None = None, value: str = "", initial_option: str = "", ) -> dict[str, Any]: sel: dict[str, Any] = {"tag": "select_static"} if placeholder: sel["placeholder"] = _build_plain_text(placeholder) if options: sel["options"] = _build_options(options) if value: sel["value"] = value if initial_option: sel["initial_option"] = initial_option return sel def make_select_person( placeholder: str = "", value: str = "", ) -> dict[str, Any]: sel: dict[str, Any] = {"tag": "select_person"} if placeholder: sel["placeholder"] = _build_plain_text(placeholder) if value: sel["value"] = value return sel def make_date_picker( placeholder: str = "", value: str = "", initial_date: str = "", ) -> dict[str, Any]: sel: dict[str, Any] = {"tag": "date_picker"} if placeholder: sel["placeholder"] = _build_plain_text(placeholder) if value: sel["value"] = value if initial_date: sel["initial_date"] = initial_date return sel def make_time_picker( placeholder: str = "", value: str = "", initial_time: str = "", ) -> dict[str, Any]: sel: dict[str, Any] = {"tag": "time_picker"} if placeholder: sel["placeholder"] = _build_plain_text(placeholder) if value: sel["value"] = value if initial_time: sel["initial_time"] = initial_time return sel def make_datetime_picker( placeholder: str = "", value: str = "", initial_datetime: str = "", ) -> dict[str, Any]: sel: dict[str, Any] = {"tag": "datetime_picker"} if placeholder: sel["placeholder"] = _build_plain_text(placeholder) if value: sel["value"] = value if initial_datetime: sel["initial_datetime"] = initial_datetime return sel def make_overflow_menu( options: list[dict[str, str]] | None = None, value: str = "", ) -> dict[str, Any]: sel: dict[str, Any] = {"tag": "overflow"} if options: sel["options"] = _build_options(options) if value: sel["value"] = value return sel def make_input_field( placeholder: str = "", value: str = "", multiline: bool = False, max_length: int = 0, ) -> dict[str, Any]: field: dict[str, Any] = {"tag": "input"} if placeholder: field["placeholder"] = _build_plain_text(placeholder) if value: field["value"] = value if multiline: field["multiline"] = True if max_length: field["max_length"] = max_length return field def make_checkbox_group( options: list[dict[str, str]] | None = None, values: list[str] | None = None, ) -> dict[str, Any]: group: dict[str, Any] = {"tag": "checkbox"} if options: group["options"] = _build_options(options) if values: group["value"] = values return group def make_image_element(img_key: str, alt_text: str = "") -> dict[str, Any]: return { "tag": "img", "img_key": img_key, "alt": _build_plain_text(alt_text), } def build_feishu_text_content(content: str) -> dict[str, Any]: return {"text": content} def build_feishu_post_content(content: str) -> dict[str, Any]: md_lines = content.split("\n") paragraphs: list[list[dict[str, Any]]] = [] for line in md_lines: if not line.strip(): paragraphs.append([{"tag": "text", "text": ""}]) else: paragraphs.append([{"tag": "md", "text": line}]) return {"zh_cn": {"content": paragraphs}} def is_post_format_requested(metadata: dict | None = None) -> bool: if not metadata: return False return metadata.get("use_post_format", False) or metadata.get("usePostFormat", False) def _truncate_card_content(content: str, limit: int = CARD_CONTENT_LIMIT) -> str: if len(content) <= limit: return content suffix = "\n\n...(内容过长已截断)" return content[: limit - len(suffix)] + suffix def build_feishu_card( content: str, *, title: str = "AI 助手", buttons: list[dict[str, str]] | None = None, streaming: bool = False, url_unfurl: list[str] | None = None, images: list[str] | None = None, files: list[dict[str, str]] | None = None, note: str | None = None, template: str | None = None, tone: str | None = None, context_text: str | None = None, dividers: int = 0, selectors: list[dict[str, Any]] | None = None, visible_to_operator: bool = False, ) -> dict[str, Any]: elements: list[dict[str, Any]] = [] if context_text: elements.append( { "tag": "note", "elements": [_build_plain_text(context_text)], } ) for _ in range(dividers): elements.append({"tag": "hr"}) if images: for img_key in images: elements.append(make_image_element(img_key)) truncated_content = _truncate_card_content(content) elements.append({"tag": "markdown", "content": truncated_content}) if files: for f in files: name = f.get("name", "文件") url = f.get("url", "") link = f"[📎 {name}]({url})" if url else f"📎 {name}" elements.append({"tag": "markdown", "content": link}) if selectors: for sel in selectors: selector_tag = resolve_selector_tag(sel.get("tag", "select_static")) selector_element: dict[str, Any] = {"tag": selector_tag} if "placeholder" in sel: if isinstance(sel["placeholder"], dict): selector_element["placeholder"] = sel["placeholder"] else: selector_element["placeholder"] = _build_plain_text(str(sel["placeholder"])) if "options" in sel: selector_element["options"] = _build_options(sel["options"]) for key in ("value", "initial_option", "initial_date", "initial_time", "initial_datetime"): if key in sel: selector_element[key] = sel[key] elements.append(selector_element) if buttons: actions: list[dict[str, Any]] = [] for btn in buttons: btn_type = resolve_button_type(btn.get("style")) actions.append( { "tag": "button", "text": _build_plain_text(btn.get("text", "")), "type": btn_type, "value": {"action": btn.get("action", "")}, } ) elements.append({"tag": "action", "actions": actions}) if url_unfurl: for url in url_unfurl: elements.append({"tag": "markdown", "content": f"[🔗 {url}]({url})"}) if note: elements.append( { "tag": "note", "elements": [_build_plain_text(note)], } ) header_title = f"{title} (回复中...)" if streaming else title color = resolve_template_color(template, tone) card: dict[str, Any] = { "header": { "title": _build_plain_text(header_title), "template": color, }, "elements": elements, } if visible_to_operator: card["config"] = {"update_multi": False} return card