mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 21:07:28 +08:00
feat(feishu): render markdown replies as cards
This commit is contained in:
@@ -28,6 +28,7 @@ from bridge.context import ContextType
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from channel.chat_channel import ChatChannel, check_prefix
|
||||
from channel.feishu.feishu_message import FeishuMessage
|
||||
from channel.feishu.feishu_static_card import build_text_delivery
|
||||
from common import utils
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
@@ -628,7 +629,13 @@ class FeiShuChanel(ChatChannel):
|
||||
logger.debug(f"[FeiShu] sending reply, type={context.type}, content={reply.content[:100]}...")
|
||||
reply_content = reply.content
|
||||
content_key = "text"
|
||||
if reply.type == ReplyType.IMAGE_URL:
|
||||
prepared_content_json = None
|
||||
if reply.type == ReplyType.TEXT:
|
||||
msg_type, prepared_content_json = build_text_delivery(
|
||||
reply.content,
|
||||
enabled=conf().get("feishu_markdown_card", True),
|
||||
)
|
||||
elif reply.type == ReplyType.IMAGE_URL:
|
||||
# 图片上传
|
||||
reply_content = self._upload_image_url(reply.content, access_token)
|
||||
if not reply_content:
|
||||
@@ -690,7 +697,11 @@ class FeiShuChanel(ChatChannel):
|
||||
can_reply = is_group and msg and hasattr(msg, 'msg_id') and msg.msg_id
|
||||
|
||||
# Build content JSON
|
||||
content_json = json.dumps(reply_content, ensure_ascii=False) if content_key is None else json.dumps({content_key: reply_content}, ensure_ascii=False)
|
||||
content_json = prepared_content_json or (
|
||||
json.dumps(reply_content, ensure_ascii=False)
|
||||
if content_key is None
|
||||
else json.dumps({content_key: reply_content}, ensure_ascii=False)
|
||||
)
|
||||
logger.debug(f"[FeiShu] Sending message: msg_type={msg_type}, content={content_json[:200]}")
|
||||
|
||||
if can_reply:
|
||||
@@ -714,6 +725,39 @@ class FeiShuChanel(ChatChannel):
|
||||
res = res.json()
|
||||
if res.get("code") == 0:
|
||||
logger.info(f"[FeiShu] send message success")
|
||||
elif msg_type == "interactive" and reply.type == ReplyType.TEXT:
|
||||
logger.warning(
|
||||
"[FeiShu] Markdown card failed, falling back to text, "
|
||||
f"code={res.get('code')}, msg={res.get('msg')}"
|
||||
)
|
||||
fallback_data = {
|
||||
"msg_type": "text",
|
||||
"content": json.dumps({"text": reply.content}, ensure_ascii=False),
|
||||
}
|
||||
if can_reply:
|
||||
fallback_res = requests.post(
|
||||
url=url,
|
||||
headers=headers,
|
||||
json=fallback_data,
|
||||
timeout=(5, 10),
|
||||
)
|
||||
else:
|
||||
fallback_data["receive_id"] = context.get("receiver")
|
||||
fallback_res = requests.post(
|
||||
url=url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
json=fallback_data,
|
||||
timeout=(5, 10),
|
||||
)
|
||||
fallback_body = fallback_res.json()
|
||||
if fallback_body.get("code") == 0:
|
||||
logger.info("[FeiShu] text fallback sent successfully")
|
||||
else:
|
||||
logger.error(
|
||||
"[FeiShu] text fallback failed, "
|
||||
f"code={fallback_body.get('code')}, msg={fallback_body.get('msg')}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"[FeiShu] send message failed, code={res.get('code')}, msg={res.get('msg')}")
|
||||
|
||||
|
||||
46
channel/feishu/feishu_static_card.py
Normal file
46
channel/feishu/feishu_static_card.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Helpers for choosing the native Feishu delivery format for text replies."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
_BLOCK_MARKDOWN = re.compile(
|
||||
r"(?m)^\s{0,3}(?:#{1,6}\s|>\s|[-*+]\s|\d+[.)]\s|```|~~~)"
|
||||
)
|
||||
_INLINE_MARKDOWN = re.compile(r"(`[^`\n]+`|\*\*[^*\n]+\*\*|\[[^]\n]+\]\([^)\n]+\))")
|
||||
_TABLE_SEPARATOR = re.compile(r"(?m)^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$")
|
||||
|
||||
|
||||
def contains_markdown(text: str) -> bool:
|
||||
"""Return whether *text* contains syntax that benefits from card Markdown."""
|
||||
if not text:
|
||||
return False
|
||||
return bool(
|
||||
_BLOCK_MARKDOWN.search(text)
|
||||
or _INLINE_MARKDOWN.search(text)
|
||||
or _TABLE_SEPARATOR.search(text)
|
||||
)
|
||||
|
||||
|
||||
def build_markdown_card(text: str) -> dict:
|
||||
"""Build an inline Card 2.0 payload with one Markdown element."""
|
||||
return {
|
||||
"schema": "2.0",
|
||||
"config": {},
|
||||
"body": {
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": text,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_text_delivery(text: str, enabled: bool = True) -> Tuple[str, str]:
|
||||
"""Return the Feishu ``msg_type`` and serialized content for a text reply."""
|
||||
if enabled and contains_markdown(text):
|
||||
return "interactive", json.dumps(build_markdown_card(text), ensure_ascii=False)
|
||||
return "text", json.dumps({"text": text}, ensure_ascii=False)
|
||||
Reference in New Issue
Block a user