这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
318 lines
8.5 KiB
Python
318 lines
8.5 KiB
Python
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,
|
|
) -> 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)
|
|
|
|
return {
|
|
"header": {
|
|
"title": _build_plain_text(header_title),
|
|
"template": color,
|
|
},
|
|
"elements": elements,
|
|
} |