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).
|
/// Extract text from a single post element (text, link, at-mention).
|
||||||
fn extract_element(el: &serde_json::Value, out: &mut Vec<String>) {
|
fn extract_element(el: &serde_json::Value, out: &mut Vec<String>) {
|
||||||
match el.get("tag").and_then(|t| t.as_str()).unwrap_or("") {
|
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" => {
|
"text" => {
|
||||||
if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
|
if let Some(text) = el.get("text").and_then(|t| t.as_str()) {
|
||||||
out.push(text.to_string());
|
out.push(text.to_string());
|
||||||
@ -1582,8 +1587,8 @@ struct MdPatterns {
|
|||||||
impl MdPatterns {
|
impl MdPatterns {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
table_re: Regex::new(r"((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)").unwrap(),
|
table_re: Regex::new(r"(?m)((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)").unwrap(),
|
||||||
heading_re: Regex::new(r"^(#{1,6})\s+(.+)$").unwrap(),
|
heading_re: Regex::new(r"(?m)^(#{1,6})\s+(.+)$").unwrap(),
|
||||||
code_block_re: Regex::new(r"(```[\s\S]*?```)").unwrap(),
|
code_block_re: Regex::new(r"(```[\s\S]*?```)").unwrap(),
|
||||||
bold_re: Regex::new(r"\*\*(.+?)\*\*|__(.+?)__").unwrap(),
|
bold_re: Regex::new(r"\*\*(.+?)\*\*|__(.+?)__").unwrap(),
|
||||||
// Simple pattern: detect *text* or _text_ markers (may overlap with bold, but that's ok for detection)
|
// 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(),
|
italic_re: Regex::new(r"\*[^*]+\*|_[^_]+_").unwrap(),
|
||||||
strike_re: Regex::new(r"~~.+?~~").unwrap(),
|
strike_re: Regex::new(r"~~.+?~~").unwrap(),
|
||||||
link_re: Regex::new(r"\[([^\]]+)\]\((https?://[^\)]+)\)").unwrap(),
|
link_re: Regex::new(r"\[([^\]]+)\]\((https?://[^\)]+)\)").unwrap(),
|
||||||
list_re: Regex::new(r"^[\s]*[-*+]\s+").unwrap(),
|
list_re: Regex::new(r"(?m)^[\s]*[-*+]\s+").unwrap(),
|
||||||
olist_re: Regex::new(r"^[\s]*\d+\.\s+").unwrap(),
|
olist_re: Regex::new(r"(?m)^[\s]*\d+\.\s+").unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1609,11 +1614,8 @@ impl FeishuChannel {
|
|||||||
let patterns = MdPatterns::new();
|
let patterns = MdPatterns::new();
|
||||||
let stripped = content.trim();
|
let stripped = content.trim();
|
||||||
|
|
||||||
// Complex markdown (code blocks, tables, headings) → always interactive card
|
// Tables and headings are not supported by post `md` nodes, so use cards.
|
||||||
if patterns.table_re.is_match(stripped)
|
if patterns.table_re.is_match(stripped) || patterns.heading_re.is_match(stripped) {
|
||||||
|| patterns.heading_re.is_match(stripped)
|
|
||||||
|| patterns.code_block_re.is_match(stripped)
|
|
||||||
{
|
|
||||||
return MsgFormat::Interactive;
|
return MsgFormat::Interactive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1622,21 +1624,15 @@ impl FeishuChannel {
|
|||||||
return MsgFormat::Interactive;
|
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)
|
if patterns.bold_re.is_match(stripped)
|
||||||
|| patterns.italic_re.is_match(stripped)
|
|| patterns.italic_re.is_match(stripped)
|
||||||
|| patterns.strike_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;
|
return MsgFormat::Post;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1866,62 +1862,14 @@ impl FeishuChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert markdown content to Feishu post message JSON.
|
/// 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 {
|
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!({
|
let post_body = serde_json::json!({
|
||||||
"zh_cn": {
|
"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]
|
#[async_trait]
|
||||||
impl Channel for FeishuChannel {
|
impl Channel for FeishuChannel {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user