飞书渠道简化
This commit is contained in:
parent
52925fcd73
commit
2bff987be1
@ -780,32 +780,11 @@ impl FeishuChannel {
|
|||||||
Some(format!("[Reply to: {}]", text))
|
Some(format!("[Reply to: {}]", text))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a message to Feishu chat with specified message type
|
/// Send a message to Feishu chat with specified message type and content.
|
||||||
|
/// Content is passed as-is (already a JSON string for file/media, or plain text for fallback).
|
||||||
async fn send_message_to_feishu(&self, receive_id: &str, receive_id_type: &str, msg_type: &str, content: &str) -> Result<(), ChannelError> {
|
async fn send_message_to_feishu(&self, receive_id: &str, receive_id_type: &str, msg_type: &str, content: &str) -> Result<(), ChannelError> {
|
||||||
let token = self.get_tenant_access_token().await?;
|
let token = self.get_tenant_access_token().await?;
|
||||||
|
|
||||||
// Feishu text messages have content limits (~64KB).
|
|
||||||
// Truncate if content is too long to avoid API error 230001.
|
|
||||||
const MAX_TEXT_LENGTH: usize = 60_000;
|
|
||||||
|
|
||||||
let payload_content = if msg_type == "text" {
|
|
||||||
let truncated = if content.len() > MAX_TEXT_LENGTH {
|
|
||||||
format!("{}...\n\n[Content truncated due to length limit]", &content[..content.ceil_char_boundary(MAX_TEXT_LENGTH)])
|
|
||||||
} else {
|
|
||||||
content.to_string()
|
|
||||||
};
|
|
||||||
serde_json::json!({ "text": truncated }).to_string()
|
|
||||||
} else {
|
|
||||||
// For post messages, content is already JSON (from markdown_to_post)
|
|
||||||
// But we still need to check length
|
|
||||||
if content.len() > MAX_TEXT_LENGTH {
|
|
||||||
// Fallback to truncated text for post as well
|
|
||||||
serde_json::json!({ "text": format!("{}...\n\n[Content truncated due to length limit]", &content[..content.ceil_char_boundary(MAX_TEXT_LENGTH)]) }).to_string()
|
|
||||||
} else {
|
|
||||||
content.to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let resp = self.http_client
|
let resp = self.http_client
|
||||||
.post(format!("{}/im/v1/messages?receive_id_type={}", FEISHU_API_BASE, receive_id_type))
|
.post(format!("{}/im/v1/messages?receive_id_type={}", FEISHU_API_BASE, receive_id_type))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
@ -813,7 +792,7 @@ impl FeishuChannel {
|
|||||||
.json(&serde_json::json!({
|
.json(&serde_json::json!({
|
||||||
"receive_id": receive_id,
|
"receive_id": receive_id,
|
||||||
"msg_type": msg_type,
|
"msg_type": msg_type,
|
||||||
"content": payload_content
|
"content": content
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@ -1564,67 +1543,6 @@ fn strip_at_placeholders(text: &str) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Markdown parsing and Feishu card element building
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Message format types for Feishu
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum MsgFormat {
|
|
||||||
/// Plain text, short and no markdown
|
|
||||||
Text,
|
|
||||||
/// Rich text (links only, moderate length)
|
|
||||||
Post,
|
|
||||||
/// Interactive card with full markdown rendering
|
|
||||||
Interactive,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Regex patterns for markdown detection
|
|
||||||
struct MdPatterns {
|
|
||||||
/// Regex to match markdown tables (header + separator + data rows)
|
|
||||||
table_re: Regex,
|
|
||||||
/// Regex to match headings (# heading)
|
|
||||||
heading_re: Regex,
|
|
||||||
/// Regex to match code blocks (```...```)
|
|
||||||
code_block_re: Regex,
|
|
||||||
/// Regex to match bold **text** or __text__
|
|
||||||
bold_re: Regex,
|
|
||||||
/// Regex to match italic *text* or _text_
|
|
||||||
italic_re: Regex,
|
|
||||||
/// Regex to match strikethrough ~~text~~
|
|
||||||
strike_re: Regex,
|
|
||||||
/// Regex to match markdown links [text](url)
|
|
||||||
link_re: Regex,
|
|
||||||
/// Regex to match unordered list items (- item or * item)
|
|
||||||
list_re: Regex,
|
|
||||||
/// Regex to match ordered list items (1. item)
|
|
||||||
olist_re: Regex,
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
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)
|
|
||||||
// We use a conservative approach: detect potential italic markers
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MdPatterns {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FeishuChannel {
|
impl FeishuChannel {
|
||||||
fn strip_thinking_tags(content: &str) -> String {
|
fn strip_thinking_tags(content: &str) -> String {
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
@ -1635,328 +1553,69 @@ impl FeishuChannel {
|
|||||||
stripped.trim().to_string()
|
stripped.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine the optimal Feishu message format for content.
|
/// Build a Card JSON 2.0 interactive card with a single markdown element.
|
||||||
fn detect_msg_format(content: &str) -> MsgFormat {
|
fn build_card_content(markdown: &str) -> String {
|
||||||
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)
|
|
||||||
{
|
|
||||||
return MsgFormat::Interactive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Long content → interactive card (better readability)
|
|
||||||
if stripped.len() > 2000 {
|
|
||||||
return MsgFormat::Interactive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has bold/italic/strikethrough → interactive card (post format can't render these)
|
|
||||||
if patterns.bold_re.is_match(stripped)
|
|
||||||
|| patterns.italic_re.is_match(stripped)
|
|
||||||
|| patterns.strike_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Short plain text → text format
|
|
||||||
if stripped.len() <= 200 {
|
|
||||||
return MsgFormat::Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Medium plain text without formatting → post format
|
|
||||||
MsgFormat::Post
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strip markdown formatting markers from text for plain display.
|
|
||||||
fn strip_md_formatting(text: &str) -> String {
|
|
||||||
let patterns = MdPatterns::new();
|
|
||||||
let mut result = text.to_string();
|
|
||||||
|
|
||||||
// Remove bold markers
|
|
||||||
result = patterns.bold_re.replace_all(&result, "$1$2").to_string();
|
|
||||||
// Remove italic markers
|
|
||||||
result = patterns.italic_re.replace_all(&result, "$1$2").to_string();
|
|
||||||
// Remove strikethrough markers
|
|
||||||
result = patterns.strike_re.replace_all(&result, "$1").to_string();
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a markdown table into a Feishu table element.
|
|
||||||
fn parse_md_table(table_text: &str) -> Option<serde_json::Value> {
|
|
||||||
let lines: Vec<&str> = table_text
|
|
||||||
.trim()
|
|
||||||
.split('\n')
|
|
||||||
.map(|l| l.trim())
|
|
||||||
.filter(|l| !l.is_empty())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if lines.len() < 3 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split(line: &str) -> Vec<String> {
|
|
||||||
line.trim_start_matches('|')
|
|
||||||
.trim_end_matches('|')
|
|
||||||
.split('|')
|
|
||||||
.map(|c| c.trim().to_string())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
let headers = split(lines[0]);
|
|
||||||
let rows: Vec<std::collections::HashMap<String, String>> = lines[2..]
|
|
||||||
.iter()
|
|
||||||
.map(|line| {
|
|
||||||
let cells: Vec<String> = split(line);
|
|
||||||
headers
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, _h)| {
|
|
||||||
(
|
|
||||||
format!("c{}", i),
|
|
||||||
if i < cells.len() {
|
|
||||||
Self::strip_md_formatting(&cells[i])
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let columns: Vec<serde_json::Value> = headers
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, h)| {
|
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"tag": "column",
|
"schema": "2.0",
|
||||||
"name": format!("c{}", i),
|
"body": {
|
||||||
"display_name": Self::strip_md_formatting(h),
|
"elements": [{
|
||||||
"width": "auto"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(serde_json::json!({
|
|
||||||
"tag": "table",
|
|
||||||
"page_size": rows.len() + 1,
|
|
||||||
"columns": columns,
|
|
||||||
"rows": rows,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split content by headings, converting headings to div elements.
|
|
||||||
fn split_headings(content: &str) -> Vec<serde_json::Value> {
|
|
||||||
let patterns = MdPatterns::new();
|
|
||||||
let mut protected = content.to_string();
|
|
||||||
|
|
||||||
// Protect code blocks by replacing them with placeholders
|
|
||||||
let mut code_blocks: Vec<String> = Vec::new();
|
|
||||||
for m in patterns.code_block_re.find_iter(content) {
|
|
||||||
code_blocks.push(m.as_str().to_string());
|
|
||||||
protected = protected.replace(m.as_str(), &format!("\x00CODE{}\x00", code_blocks.len() - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut elements: Vec<serde_json::Value> = Vec::new();
|
|
||||||
let mut last_end = 0;
|
|
||||||
|
|
||||||
for m in patterns.heading_re.find_iter(&protected) {
|
|
||||||
let before = &protected[last_end..m.start()].trim();
|
|
||||||
if !before.is_empty() {
|
|
||||||
elements.push(serde_json::json!({
|
|
||||||
"tag": "markdown",
|
"tag": "markdown",
|
||||||
"content": before
|
"content": markdown
|
||||||
}));
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
let heading_text = Self::strip_md_formatting(m.as_str().trim_start_matches('#').trim());
|
/// Max byte-size for markdown content in a single card.
|
||||||
let display_text = if heading_text.is_empty() {
|
/// Card payloads have a ~30 KB limit; leave margin for JSON envelope.
|
||||||
String::new()
|
const CARD_MARKDOWN_MAX_BYTES: usize = 28_000;
|
||||||
|
|
||||||
|
/// Split markdown content into chunks that fit within the card size limit.
|
||||||
|
/// Splits on line boundaries to avoid breaking markdown syntax.
|
||||||
|
fn split_markdown_chunks(text: &str) -> Vec<String> {
|
||||||
|
if text.len() <= Self::CARD_MARKDOWN_MAX_BYTES {
|
||||||
|
return vec![text.to_string()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chunks: Vec<String> = Vec::new();
|
||||||
|
let mut start = 0;
|
||||||
|
|
||||||
|
while start < text.len() {
|
||||||
|
if start + Self::CARD_MARKDOWN_MAX_BYTES >= text.len() {
|
||||||
|
chunks.push(text[start..].to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = start + Self::CARD_MARKDOWN_MAX_BYTES;
|
||||||
|
let search_region = &text[start..end];
|
||||||
|
let split_at = search_region
|
||||||
|
.rfind('\n')
|
||||||
|
.map(|pos| start + pos + 1)
|
||||||
|
.unwrap_or(end);
|
||||||
|
|
||||||
|
let split_at = if text.is_char_boundary(split_at) {
|
||||||
|
split_at
|
||||||
} else {
|
} else {
|
||||||
format!("**{}**", heading_text)
|
(start..split_at)
|
||||||
|
.rev()
|
||||||
|
.find(|&i| text.is_char_boundary(i))
|
||||||
|
.unwrap_or(start)
|
||||||
};
|
};
|
||||||
|
|
||||||
elements.push(serde_json::json!({
|
if split_at <= start {
|
||||||
"tag": "div",
|
let forced = (end..=text.len())
|
||||||
"text": {
|
.find(|&i| text.is_char_boundary(i))
|
||||||
"tag": "lark_md",
|
.unwrap_or(text.len());
|
||||||
"content": display_text
|
chunks.push(text[start..forced].to_string());
|
||||||
}
|
start = forced;
|
||||||
}));
|
|
||||||
|
|
||||||
last_end = m.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = protected[last_end..].trim();
|
|
||||||
if !remaining.is_empty() {
|
|
||||||
// Restore code blocks
|
|
||||||
let mut final_content = remaining.to_string();
|
|
||||||
for (i, cb) in code_blocks.iter().enumerate() {
|
|
||||||
final_content = final_content.replace(&format!("\x00CODE{}\x00", i), cb);
|
|
||||||
}
|
|
||||||
elements.push(serde_json::json!({
|
|
||||||
"tag": "markdown",
|
|
||||||
"content": final_content
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if elements.is_empty() {
|
|
||||||
elements.push(serde_json::json!({
|
|
||||||
"tag": "markdown",
|
|
||||||
"content": content
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
elements
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split card elements into groups with at most one table element each.
|
|
||||||
/// Feishu cards have a hard limit of one table per card (API error 11310).
|
|
||||||
fn split_elements_by_table_limit(elements: &[serde_json::Value]) -> Vec<Vec<serde_json::Value>> {
|
|
||||||
if elements.is_empty() {
|
|
||||||
return vec![vec![]];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut groups: Vec<Vec<serde_json::Value>> = Vec::new();
|
|
||||||
let mut current: Vec<serde_json::Value> = Vec::new();
|
|
||||||
let mut table_count = 0;
|
|
||||||
|
|
||||||
for el in elements {
|
|
||||||
if el.get("tag").and_then(|t| t.as_str()) == Some("table") {
|
|
||||||
if table_count >= 1 {
|
|
||||||
groups.push(current);
|
|
||||||
current = Vec::new();
|
|
||||||
table_count = 0;
|
|
||||||
}
|
|
||||||
current.push(el.clone());
|
|
||||||
table_count += 1;
|
|
||||||
} else {
|
} else {
|
||||||
current.push(el.clone());
|
chunks.push(text[start..split_at].to_string());
|
||||||
|
start = split_at;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !current.is_empty() {
|
chunks
|
||||||
groups.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
groups
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build content into card elements (div/markdown + table).
|
|
||||||
fn build_card_elements(content: &str) -> Vec<serde_json::Value> {
|
|
||||||
let patterns = MdPatterns::new();
|
|
||||||
let mut elements: Vec<serde_json::Value> = Vec::new();
|
|
||||||
let mut last_end = 0;
|
|
||||||
|
|
||||||
// Find all tables in content
|
|
||||||
for m in patterns.table_re.find_iter(content) {
|
|
||||||
let before = &content[last_end..m.start()];
|
|
||||||
if !before.trim().is_empty() {
|
|
||||||
elements.extend(Self::split_headings(before));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(table) = Self::parse_md_table(m.as_str()) {
|
|
||||||
elements.push(table);
|
|
||||||
} else {
|
|
||||||
elements.push(serde_json::json!({
|
|
||||||
"tag": "markdown",
|
|
||||||
"content": m.as_str()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
last_end = m.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = &content[last_end..];
|
|
||||||
if !remaining.trim().is_empty() {
|
|
||||||
elements.extend(Self::split_headings(remaining));
|
|
||||||
}
|
|
||||||
|
|
||||||
if elements.is_empty() {
|
|
||||||
elements.push(serde_json::json!({
|
|
||||||
"tag": "markdown",
|
|
||||||
"content": content
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
elements
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert markdown content to Feishu post message JSON.
|
|
||||||
/// Handles links [text](url) as <a> tags; everything else as text tags.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
post_body.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send an interactive card message to Feishu.
|
/// Send an interactive card message to Feishu.
|
||||||
@ -2097,7 +1756,7 @@ impl Channel for FeishuChannel {
|
|||||||
let receive_id = if msg.chat_id.starts_with("oc_") { &msg.chat_id } else { msg.reply_to.as_ref().unwrap_or(&msg.chat_id) };
|
let receive_id = if msg.chat_id.starts_with("oc_") { &msg.chat_id } else { msg.reply_to.as_ref().unwrap_or(&msg.chat_id) };
|
||||||
let receive_id_type = if msg.chat_id.starts_with("oc_") { "chat_id" } else { "open_id" };
|
let receive_id_type = if msg.chat_id.starts_with("oc_") { "chat_id" } else { "open_id" };
|
||||||
|
|
||||||
// If no media, use smart format detection
|
// If no media, send as interactive card with raw markdown
|
||||||
if msg.media.is_empty() {
|
if msg.media.is_empty() {
|
||||||
let content = msg.content.trim();
|
let content = msg.content.trim();
|
||||||
|
|
||||||
@ -2107,34 +1766,13 @@ impl Channel for FeishuChannel {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let fmt = Self::detect_msg_format(content);
|
let chunks = Self::split_markdown_chunks(content);
|
||||||
|
for chunk in &chunks {
|
||||||
match fmt {
|
let card = Self::build_card_content(chunk);
|
||||||
MsgFormat::Text => {
|
if let Err(e) = self.send_interactive_card(receive_id, receive_id_type, &card).await {
|
||||||
// Short plain text – send as simple text message
|
|
||||||
let result = self.send_message_to_feishu(receive_id, receive_id_type, "text", content).await;
|
|
||||||
self.remove_reaction_from_metadata(&msg.metadata).await;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
MsgFormat::Post => {
|
|
||||||
// Medium content with links – send as rich-text post
|
|
||||||
let post_body = Self::markdown_to_post(content);
|
|
||||||
let result = self.send_message_to_feishu(receive_id, receive_id_type, "post", &post_body).await;
|
|
||||||
self.remove_reaction_from_metadata(&msg.metadata).await;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
MsgFormat::Interactive => {
|
|
||||||
// Complex / long content – send as interactive card
|
|
||||||
let elements = Self::build_card_elements(content);
|
|
||||||
for chunk in Self::split_elements_by_table_limit(&elements) {
|
|
||||||
let card = serde_json::json!({
|
|
||||||
"config": { "wide_screen_mode": true },
|
|
||||||
"elements": chunk
|
|
||||||
});
|
|
||||||
if let Err(e) = self.send_interactive_card(receive_id, receive_id_type, &card.to_string()).await {
|
|
||||||
tracing::warn!(error = %e, "Failed to send interactive card, falling back to text");
|
tracing::warn!(error = %e, "Failed to send interactive card, falling back to text");
|
||||||
// Fallback to plain text
|
let text_content = serde_json::json!({ "text": chunk }).to_string();
|
||||||
let result = self.send_message_to_feishu(receive_id, receive_id_type, "text", content).await;
|
let result = self.send_message_to_feishu(receive_id, receive_id_type, "text", &text_content).await;
|
||||||
self.remove_reaction_from_metadata(&msg.metadata).await;
|
self.remove_reaction_from_metadata(&msg.metadata).await;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -2142,8 +1780,6 @@ impl Channel for FeishuChannel {
|
|||||||
self.remove_reaction_from_metadata(&msg.metadata).await;
|
self.remove_reaction_from_metadata(&msg.metadata).await;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle multimodal message - send with media
|
// Handle multimodal message - send with media
|
||||||
let token = self.get_tenant_access_token().await?;
|
let token = self.get_tenant_access_token().await?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user