ForcePilot/backend/package/yuxi/channels/plugins/feishu/card_builder.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

208 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""飞书 Card Kit 卡片构建模块。
将契约层 ``RichMessage`` DTO 映射为飞书 Card Kit Schema 2.0 JSON
并提供 Markdown 卡片、流式卡片与降级 Markdown 文本的构建能力。
仅依赖契约层 DTO 与标准库。
"""
from __future__ import annotations
from typing import Any
from yuxi.channels.contract.dtos.outbound import (
RichMessage,
RichMessageButton,
RichMessageCheckbox,
RichMessageDatePicker,
RichMessageInput,
RichMessageSelect,
)
def build_card_from_rich_message(rich_message: RichMessage) -> dict[str, Any]:
"""将 ``RichMessage`` 映射为飞书 Card Kit Schema 2.0 卡片。
``title`` → header.titleplain_text``text`` → body markdown
``description`` → body note``image_url`` → body img``buttons`` →
column_set``selects`` / ``date_pickers`` / ``checkboxes`` / ``inputs``
→ 对应交互元素,均追加至 body.elements。
"""
elements: list[dict[str, Any]] = []
if rich_message.text:
elements.append({"tag": "markdown", "content": rich_message.text})
if rich_message.description:
elements.append(
{
"tag": "note",
"elements": [{"tag": "plain_text", "content": rich_message.description}],
}
)
if rich_message.image_url:
elements.append(_image_element(rich_message.image_url))
if rich_message.buttons:
elements.append(_button_column_set(rich_message.buttons))
for sel in rich_message.selects:
elements.append(_select_element(sel))
for dp in rich_message.date_pickers:
elements.append(_date_picker_element(dp))
for cb in rich_message.checkboxes:
elements.append(_checkbox_element(cb))
for inp in rich_message.inputs:
elements.append(_input_element(inp))
card: dict[str, Any] = {"schema": "2.0"}
if rich_message.title:
card["header"] = {"title": {"tag": "plain_text", "content": rich_message.title}}
card["body"] = {"elements": elements}
return card
def build_markdown_card(text: str, title: str | None = None) -> dict[str, Any]:
"""构建 Markdown 卡片title 为空时不创建 header。"""
card: dict[str, Any] = {"schema": "2.0"}
if title:
card["header"] = {"title": {"tag": "plain_text", "content": title}}
card["body"] = {"elements": [{"tag": "markdown", "content": text}]}
return card
def build_streaming_card(content: str, sequence: int, is_final: bool) -> dict[str, Any]:
"""构建流式更新卡片。
``content`` 为流式文本;``is_final=True`` 时追加“已完成”标识。
``sequence`` 为流式序号,由调用方用于协议侧顺序追踪,不嵌入卡片体。
"""
elements: list[dict[str, Any]] = [{"tag": "markdown", "content": content}]
if is_final:
elements.append(
{
"tag": "note",
"elements": [{"tag": "plain_text", "content": "已完成"}],
}
)
return {"schema": "2.0", "body": {"elements": elements}}
def degrade_to_markdown(rich_message: RichMessage) -> str:
"""将 ``RichMessage`` 降级为 Markdown 文本,空字段跳过。
标题用 ``# `` 前缀,按钮用 ``- [label](value)`` 列表,
输入框用 ``- {label}: {placeholder}`` 列表。
"""
lines: list[str] = []
if rich_message.title:
lines.append(f"# {rich_message.title}")
if rich_message.text:
lines.append(rich_message.text)
if rich_message.description:
lines.append(rich_message.description)
if rich_message.buttons:
for btn in rich_message.buttons:
target = btn.value or btn.action
lines.append(f"- [{btn.label}]({target})")
if rich_message.inputs:
for inp in rich_message.inputs:
if inp.placeholder:
lines.append(f"- {inp.label}: {inp.placeholder}")
else:
lines.append(f"- {inp.label}")
return "\n".join(lines)
def _image_element(image_url: str) -> dict[str, Any]:
if image_url.startswith(("http://", "https://")):
return {"tag": "img", "url": image_url}
return {"tag": "img", "img_key": image_url}
def _button_column_set(buttons: tuple[RichMessageButton, ...]) -> dict[str, Any]:
columns: list[dict[str, Any]] = []
for btn in buttons:
value: dict[str, Any] = {"action": btn.action}
if btn.value is not None:
value["value"] = btn.value
columns.append(
{
"tag": "column",
"width": "weighted",
"weight": 1,
"elements": [
{
"tag": "button",
"text": {"tag": "plain_text", "content": btn.label},
"value": value,
"type": "default",
}
],
}
)
return {"tag": "column_set", "columns": columns}
def _select_element(sel: RichMessageSelect) -> dict[str, Any]:
el: dict[str, Any] = {
"tag": "select_static",
"name": sel.name,
"label": {"tag": "plain_text", "content": sel.label},
"placeholder": {"tag": "plain_text", "content": sel.placeholder or sel.label},
"options": [
{
"text": {"tag": "plain_text", "content": opt.label},
"value": opt.value,
}
for opt in sel.options
],
}
if sel.default_value is not None:
el["value"] = sel.default_value
return el
def _date_picker_element(dp: RichMessageDatePicker) -> dict[str, Any]:
el: dict[str, Any] = {
"tag": "date_picker",
"name": dp.name,
"label": {"tag": "plain_text", "content": dp.label},
"placeholder": {"tag": "plain_text", "content": dp.placeholder or dp.label},
}
if dp.default_value is not None:
el["value"] = dp.default_value
if dp.min_date is not None:
el["min"] = dp.min_date
if dp.max_date is not None:
el["max"] = dp.max_date
return el
def _checkbox_element(cb: RichMessageCheckbox) -> dict[str, Any]:
el: dict[str, Any] = {
"tag": "checkbox",
"name": cb.name,
"label": {"tag": "plain_text", "content": cb.label},
"options": [
{
"text": {"tag": "plain_text", "content": opt.label},
"value": opt.value,
}
for opt in cb.options
],
}
if cb.default_values:
el["value"] = list(cb.default_values)
return el
def _input_element(inp: RichMessageInput) -> dict[str, Any]:
el: dict[str, Any] = {
"tag": "input",
"name": inp.name,
"label": {"tag": "plain_text", "content": inp.label},
"placeholder": {"tag": "plain_text", "content": inp.placeholder or inp.label},
"input_type": inp.input_type,
}
if inp.default_value is not None:
el["value"] = inp.default_value
if inp.max_length is not None:
el["max_length"] = inp.max_length
return el