新增飞书机器人适配器全套功能,包括: - 基础适配器入口与工具导出 - 消息格式化、卡片渲染、回复调度逻辑 - 会话ID生成、模型覆盖策略 - 消息发送缓存、顺序队列管理 - 飞书签名验证、加解密webhook请求 - 审批权限校验、机器人菜单事件处理 - 文档评论、钉消息、语音转码处理 - 静态/动态目录管理、子代理生命周期管理 - 各类工具集:聊天、云盘、文档、知识库API封装
295 lines
8.2 KiB
Python
295 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
TEXT_CHUNK_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 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"] = {"tag": "plain_text", "content": placeholder}
|
|
if options:
|
|
sel["options"] = [
|
|
{"text": {"tag": "plain_text", "content": opt.get("text", "")}, "value": opt.get("value", "")}
|
|
for opt in 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"] = {"tag": "plain_text", "content": 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"] = {"tag": "plain_text", "content": 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"] = {"tag": "plain_text", "content": 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"] = {"tag": "plain_text", "content": 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"] = [
|
|
{"text": {"tag": "plain_text", "content": opt.get("text", "")}, "value": opt.get("value", "")}
|
|
for opt in options
|
|
]
|
|
if value:
|
|
sel["value"] = value
|
|
return sel
|
|
|
|
|
|
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 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,
|
|
) -> dict[str, Any]:
|
|
elements: list[dict[str, Any]] = []
|
|
|
|
if context_text:
|
|
elements.append(
|
|
{
|
|
"tag": "note",
|
|
"elements": [{"tag": "plain_text", "content": context_text}],
|
|
}
|
|
)
|
|
|
|
if dividers > 0:
|
|
for _ in range(dividers):
|
|
elements.append({"tag": "hr"})
|
|
|
|
if images:
|
|
for img_key in images:
|
|
elements.append(
|
|
{
|
|
"tag": "img",
|
|
"img_key": img_key,
|
|
"alt": {"tag": "plain_text", "content": ""},
|
|
}
|
|
)
|
|
|
|
elements.append({"tag": "markdown", "content": 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"] = {"tag": "plain_text", "content": str(sel["placeholder"])}
|
|
if "options" in sel:
|
|
selector_element["options"] = [
|
|
{"text": {"tag": "plain_text", "content": opt.get("text", "")}, "value": opt.get("value", "")}
|
|
for opt in sel["options"]
|
|
]
|
|
if "value" in sel:
|
|
selector_element["value"] = sel["value"]
|
|
if "initial_option" in sel:
|
|
selector_element["initial_option"] = sel["initial_option"]
|
|
if "initial_date" in sel:
|
|
selector_element["initial_date"] = sel["initial_date"]
|
|
if "initial_time" in sel:
|
|
selector_element["initial_time"] = sel["initial_time"]
|
|
if "initial_datetime" in sel:
|
|
selector_element["initial_datetime"] = sel["initial_datetime"]
|
|
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": {"tag": "plain_text", "content": 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": [{"tag": "plain_text", "content": note}],
|
|
}
|
|
)
|
|
|
|
header_title = f"{title} (回复中...)" if streaming else title
|
|
color = resolve_template_color(template, tone)
|
|
|
|
return {
|
|
"header": {
|
|
"title": {"tag": "plain_text", "content": header_title},
|
|
"template": color,
|
|
},
|
|
"elements": elements,
|
|
}
|