1. 移除多个文件中的空行、冗余导入
2. 修复文件末尾缺少换行符的问题
3. 新增并补全飞书多类工具API实现:
- 多维表格:更新、删除记录,列出视图
- 文档:更新、追加、删除块
- 云文档:上传、下载文件
- 群组:创建、添加成员、更新信息、创建公告
- 目录:重构用户部门缓存逻辑
4. 优化消息发送、回复、转发等API的错误处理和逻辑
5. 新增消息列表查询、已读状态查询等功能
328 lines
8.7 KiB
Python
328 lines
8.7 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,
|
|
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
|