feat(feishu): 支持 markdown 格式的消息处理,优化内容解析逻辑
This commit is contained in:
parent
0c724e37bb
commit
a0fe7c57bd
@ -1189,6 +1189,11 @@ fn parse_post_content(content: &str) -> String {
|
||||
/// Extract text from a single post element (text, link, at-mention).
|
||||
fn extract_element(el: &serde_json::Value, out: &mut Vec<String>) {
|
||||
match el.get("tag").and_then(|t| t.as_str()).unwrap_or("") {
|
||||
"md" => {
|
||||
if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
|
||||
out.push(text.to_string());
|
||||
}
|
||||
}
|
||||
"text" => {
|
||||
if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
|
||||
out.push(text.to_string());
|
||||
@ -1582,8 +1587,8 @@ struct MdPatterns {
|
||||
impl MdPatterns {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
table_re: Regex::new(r"((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)").unwrap(),
|
||||
heading_re: Regex::new(r"^(#{1,6})\s+(.+)$").unwrap(),
|
||||
table_re: Regex::new(r"(?m)((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)").unwrap(),
|
||||
heading_re: Regex::new(r"(?m)^(#{1,6})\s+(.+)$").unwrap(),
|
||||
code_block_re: Regex::new(r"(```[\s\S]*?```)").unwrap(),
|
||||
bold_re: Regex::new(r"\*\*(.+?)\*\*|__(.+?)__").unwrap(),
|
||||
// Simple pattern: detect *text* or _text_ markers (may overlap with bold, but that's ok for detection)
|
||||
@ -1591,8 +1596,8 @@ impl MdPatterns {
|
||||
italic_re: Regex::new(r"\*[^*]+\*|_[^_]+_").unwrap(),
|
||||
strike_re: Regex::new(r"~~.+?~~").unwrap(),
|
||||
link_re: Regex::new(r"\[([^\]]+)\]\((https?://[^\)]+)\)").unwrap(),
|
||||
list_re: Regex::new(r"^[\s]*[-*+]\s+").unwrap(),
|
||||
olist_re: Regex::new(r"^[\s]*\d+\.\s+").unwrap(),
|
||||
list_re: Regex::new(r"(?m)^[\s]*[-*+]\s+").unwrap(),
|
||||
olist_re: Regex::new(r"(?m)^[\s]*\d+\.\s+").unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1609,11 +1614,8 @@ impl FeishuChannel {
|
||||
let patterns = MdPatterns::new();
|
||||
let stripped = content.trim();
|
||||
|
||||
// Complex markdown (code blocks, tables, headings) → always interactive card
|
||||
if patterns.table_re.is_match(stripped)
|
||||
|| patterns.heading_re.is_match(stripped)
|
||||
|| patterns.code_block_re.is_match(stripped)
|
||||
{
|
||||
// Tables and headings are not supported by post `md` nodes, so use cards.
|
||||
if patterns.table_re.is_match(stripped) || patterns.heading_re.is_match(stripped) {
|
||||
return MsgFormat::Interactive;
|
||||
}
|
||||
|
||||
@ -1622,21 +1624,15 @@ impl FeishuChannel {
|
||||
return MsgFormat::Interactive;
|
||||
}
|
||||
|
||||
// Has bold/italic/strikethrough → interactive card (post format can't render these)
|
||||
// Markdown that is supported by Feishu post `md` should be sent as post.
|
||||
if patterns.bold_re.is_match(stripped)
|
||||
|| patterns.italic_re.is_match(stripped)
|
||||
|| patterns.strike_re.is_match(stripped)
|
||||
|| patterns.code_block_re.is_match(stripped)
|
||||
|| patterns.list_re.is_match(stripped)
|
||||
|| patterns.olist_re.is_match(stripped)
|
||||
|| patterns.link_re.is_match(stripped)
|
||||
{
|
||||
return MsgFormat::Interactive;
|
||||
}
|
||||
|
||||
// Has list items → interactive card (post format can't render list bullets well)
|
||||
if patterns.list_re.is_match(stripped) || patterns.olist_re.is_match(stripped) {
|
||||
return MsgFormat::Interactive;
|
||||
}
|
||||
|
||||
// Has links → post format (supports <a> tags)
|
||||
if patterns.link_re.is_match(stripped) {
|
||||
return MsgFormat::Post;
|
||||
}
|
||||
|
||||
@ -1866,62 +1862,14 @@ impl FeishuChannel {
|
||||
}
|
||||
|
||||
/// Convert markdown content to Feishu post message JSON.
|
||||
/// Handles links [text](url) as <a> tags; everything else as text tags.
|
||||
/// Feishu Markdown is sent via a post message with an `md` node.
|
||||
fn markdown_to_post(content: &str) -> String {
|
||||
let patterns = MdPatterns::new();
|
||||
let lines = content.trim().split('\n');
|
||||
let mut paragraphs: Vec<Vec<serde_json::Value>> = Vec::new();
|
||||
|
||||
for line in lines {
|
||||
let mut elements: Vec<serde_json::Value> = Vec::new();
|
||||
let mut last_end = 0;
|
||||
|
||||
for cap in patterns.link_re.captures_iter(line) {
|
||||
// Text before this link
|
||||
let m = cap.get(0).unwrap();
|
||||
let before = &line[last_end..m.start()];
|
||||
if !before.is_empty() {
|
||||
elements.push(serde_json::json!({
|
||||
"tag": "text",
|
||||
"text": before
|
||||
}));
|
||||
}
|
||||
|
||||
let link_text = cap.get(1).map(|g| g.as_str()).unwrap_or("");
|
||||
let link_href = cap.get(2).map(|g| g.as_str()).unwrap_or("");
|
||||
|
||||
elements.push(serde_json::json!({
|
||||
"tag": "a",
|
||||
"text": link_text,
|
||||
"href": link_href
|
||||
}));
|
||||
|
||||
last_end = m.end();
|
||||
}
|
||||
|
||||
// Remaining text after last link
|
||||
let remaining = &line[last_end..];
|
||||
if !remaining.is_empty() {
|
||||
elements.push(serde_json::json!({
|
||||
"tag": "text",
|
||||
"text": remaining
|
||||
}));
|
||||
}
|
||||
|
||||
// Empty line → empty paragraph for spacing
|
||||
if elements.is_empty() {
|
||||
elements.push(serde_json::json!({
|
||||
"tag": "text",
|
||||
"text": ""
|
||||
}));
|
||||
}
|
||||
|
||||
paragraphs.push(elements);
|
||||
}
|
||||
|
||||
let post_body = serde_json::json!({
|
||||
"zh_cn": {
|
||||
"content": paragraphs
|
||||
"content": [[{
|
||||
"tag": "md",
|
||||
"text": content.trim()
|
||||
}]]
|
||||
}
|
||||
});
|
||||
|
||||
@ -1972,6 +1920,33 @@ impl FeishuChannel {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{FeishuChannel, MsgFormat};
|
||||
|
||||
#[test]
|
||||
fn markdown_post_uses_md_tag() {
|
||||
let content = "**bold**\n1. item1\n2. item2\n[link](https://open.feishu.cn)";
|
||||
let post = FeishuChannel::markdown_to_post(content);
|
||||
let parsed: serde_json::Value = serde_json::from_str(&post).unwrap();
|
||||
|
||||
assert_eq!(parsed["zh_cn"]["content"][0][0]["tag"], "md");
|
||||
assert_eq!(parsed["zh_cn"]["content"][0][0]["text"], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_markdown_is_not_misclassified_as_plain_post() {
|
||||
let content = "intro\n1. item1\n2. item2";
|
||||
assert_eq!(FeishuChannel::detect_msg_format(content), MsgFormat::Post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headings_still_use_interactive() {
|
||||
let content = "intro\n## heading";
|
||||
assert_eq!(FeishuChannel::detect_msg_format(content), MsgFormat::Interactive);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for FeishuChannel {
|
||||
fn name(&self) -> &str {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user