feat(feishu): 支持引用回复消息 - 使用飞书 reply API (POST /im/v1/messages/{id}/reply) - 仅 AssistantResponse 使用回复接口,工具消息走普通发送 - 提取公共逻辑到 prepare_payload / check_send_response - 新增 reply_to_feishu_message / dispatch_send 方法
This commit is contained in:
parent
3630e62e18
commit
09dd15f557
@ -866,32 +866,7 @@ impl FeishuChannel {
|
||||
) -> Result<(), ChannelError> {
|
||||
let token = self.get_tenant_access_token().await?;
|
||||
|
||||
let payload_content = if msg_type == "text" {
|
||||
let truncated = if char_count(content) > self.config.max_message_chars {
|
||||
format!(
|
||||
"{}\n\n[Content truncated due to length limit]",
|
||||
truncate_with_ellipsis(content, self.config.max_message_chars)
|
||||
)
|
||||
} 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 char_count(content) > self.config.max_message_chars {
|
||||
// Fallback to truncated text for post as well
|
||||
serde_json::json!({
|
||||
"text": format!(
|
||||
"{}\n\n[Content truncated due to length limit]",
|
||||
truncate_with_ellipsis(content, self.config.max_message_chars)
|
||||
)
|
||||
})
|
||||
.to_string()
|
||||
} else {
|
||||
content.to_string()
|
||||
}
|
||||
};
|
||||
let payload_content = self.prepare_payload(msg_type, content);
|
||||
|
||||
let resp = self
|
||||
.http_client
|
||||
@ -912,6 +887,70 @@ impl FeishuChannel {
|
||||
ChannelError::ConnectionError(format!("Send message HTTP error: {}", e))
|
||||
})?;
|
||||
|
||||
Self::check_send_response(resp).await
|
||||
}
|
||||
|
||||
/// Reply to a specific message using POST /im/v1/messages/{message_id}/reply
|
||||
async fn reply_to_feishu_message(
|
||||
&self,
|
||||
reply_to_message_id: &str,
|
||||
msg_type: &str,
|
||||
content: &str,
|
||||
) -> Result<(), ChannelError> {
|
||||
let token = self.get_tenant_access_token().await?;
|
||||
|
||||
let payload_content = self.prepare_payload(msg_type, content);
|
||||
|
||||
let resp = self
|
||||
.http_client
|
||||
.post(format!(
|
||||
"{}/im/v1/messages/{}/reply",
|
||||
FEISHU_API_BASE, reply_to_message_id
|
||||
))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.json(&serde_json::json!({
|
||||
"msg_type": msg_type,
|
||||
"content": payload_content
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ChannelError::ConnectionError(format!("Reply message HTTP error: {}", e))
|
||||
})?;
|
||||
|
||||
Self::check_send_response(resp).await
|
||||
}
|
||||
|
||||
/// Prepare message payload with length truncation
|
||||
fn prepare_payload(&self, msg_type: &str, content: &str) -> String {
|
||||
if msg_type == "text" {
|
||||
let truncated = if char_count(content) > self.config.max_message_chars {
|
||||
format!(
|
||||
"{}\n\n[Content truncated due to length limit]",
|
||||
truncate_with_ellipsis(content, self.config.max_message_chars)
|
||||
)
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
serde_json::json!({ "text": truncated }).to_string()
|
||||
} else {
|
||||
if char_count(content) > self.config.max_message_chars {
|
||||
serde_json::json!({
|
||||
"text": format!(
|
||||
"{}\n\n[Content truncated due to length limit]",
|
||||
truncate_with_ellipsis(content, self.config.max_message_chars)
|
||||
)
|
||||
})
|
||||
.to_string()
|
||||
} else {
|
||||
content.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the response from Feishu send/reply API
|
||||
async fn check_send_response(resp: reqwest::Response) -> Result<(), ChannelError> {
|
||||
#[derive(Deserialize)]
|
||||
struct SendResp {
|
||||
code: i32,
|
||||
@ -933,6 +972,22 @@ impl FeishuChannel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dispatch: use reply API when reply_to is set, otherwise use send API
|
||||
async fn dispatch_send(
|
||||
&self,
|
||||
receive_id: &str,
|
||||
receive_id_type: &str,
|
||||
msg_type: &str,
|
||||
content: &str,
|
||||
reply_to: Option<&str>,
|
||||
) -> Result<(), ChannelError> {
|
||||
if let Some(parent_id) = reply_to {
|
||||
self.reply_to_feishu_message(parent_id, msg_type, content).await
|
||||
} else {
|
||||
self.send_message_to_feishu(receive_id, receive_id_type, msg_type, content).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract service_id from WebSocket URL query params
|
||||
fn extract_service_id(url: &str) -> i32 {
|
||||
url.split('?')
|
||||
@ -2412,6 +2467,13 @@ impl Channel for FeishuChannel {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Only use reply API for assistant content messages
|
||||
let reply_to = if matches!(msg.event_kind, OutboundEventKind::AssistantResponse) {
|
||||
msg.metadata.get("feishu.parent_id").map(|s| s.as_str())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let receive_id = if msg.chat_id.starts_with("oc_") {
|
||||
&msg.chat_id
|
||||
} else {
|
||||
@ -2443,7 +2505,7 @@ impl Channel for FeishuChannel {
|
||||
MsgFormat::Text => {
|
||||
// Short plain text – send as simple text message
|
||||
let result = self
|
||||
.send_message_to_feishu(receive_id, receive_id_type, "text", content)
|
||||
.dispatch_send(receive_id, receive_id_type, "text", content, reply_to)
|
||||
.await;
|
||||
remove_reaction.await;
|
||||
return result;
|
||||
@ -2452,7 +2514,7 @@ impl Channel for FeishuChannel {
|
||||
// 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)
|
||||
.dispatch_send(receive_id, receive_id_type, "post", &post_body, reply_to)
|
||||
.await;
|
||||
remove_reaction.await;
|
||||
return result;
|
||||
@ -2472,11 +2534,12 @@ impl Channel for FeishuChannel {
|
||||
tracing::warn!(error = %e, "Failed to send interactive card, falling back to text");
|
||||
// Fallback to plain text
|
||||
let result = self
|
||||
.send_message_to_feishu(
|
||||
.dispatch_send(
|
||||
receive_id,
|
||||
receive_id_type,
|
||||
"text",
|
||||
content,
|
||||
reply_to,
|
||||
)
|
||||
.await;
|
||||
remove_reaction.await;
|
||||
@ -2490,7 +2553,7 @@ impl Channel for FeishuChannel {
|
||||
}
|
||||
|
||||
if !msg.content.trim().is_empty() {
|
||||
self.send_message_to_feishu(receive_id, receive_id_type, "text", msg.content.trim())
|
||||
self.dispatch_send(receive_id, receive_id_type, "text", msg.content.trim(), reply_to)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@ -2500,21 +2563,23 @@ impl Channel for FeishuChannel {
|
||||
let result = match media_item.media_type.as_str() {
|
||||
"image" => {
|
||||
let image_key = self.upload_image(path).await?;
|
||||
self.send_message_to_feishu(
|
||||
self.dispatch_send(
|
||||
receive_id,
|
||||
receive_id_type,
|
||||
"image",
|
||||
&serde_json::json!({ "image_key": image_key }).to_string(),
|
||||
reply_to,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"audio" | "file" | "video" => {
|
||||
let file_key = self.upload_file(path).await?;
|
||||
self.send_message_to_feishu(
|
||||
self.dispatch_send(
|
||||
receive_id,
|
||||
receive_id_type,
|
||||
"file",
|
||||
&serde_json::json!({ "file_key": file_key }).to_string(),
|
||||
reply_to,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user