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> {
|
) -> Result<(), ChannelError> {
|
||||||
let token = self.get_tenant_access_token().await?;
|
let token = self.get_tenant_access_token().await?;
|
||||||
|
|
||||||
let payload_content = if msg_type == "text" {
|
let payload_content = self.prepare_payload(msg_type, content);
|
||||||
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 resp = self
|
let resp = self
|
||||||
.http_client
|
.http_client
|
||||||
@ -912,6 +887,70 @@ impl FeishuChannel {
|
|||||||
ChannelError::ConnectionError(format!("Send message HTTP error: {}", e))
|
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)]
|
#[derive(Deserialize)]
|
||||||
struct SendResp {
|
struct SendResp {
|
||||||
code: i32,
|
code: i32,
|
||||||
@ -933,6 +972,22 @@ impl FeishuChannel {
|
|||||||
Ok(())
|
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
|
/// Extract service_id from WebSocket URL query params
|
||||||
fn extract_service_id(url: &str) -> i32 {
|
fn extract_service_id(url: &str) -> i32 {
|
||||||
url.split('?')
|
url.split('?')
|
||||||
@ -2412,6 +2467,13 @@ impl Channel for FeishuChannel {
|
|||||||
return Ok(());
|
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_") {
|
let receive_id = if msg.chat_id.starts_with("oc_") {
|
||||||
&msg.chat_id
|
&msg.chat_id
|
||||||
} else {
|
} else {
|
||||||
@ -2443,7 +2505,7 @@ impl Channel for FeishuChannel {
|
|||||||
MsgFormat::Text => {
|
MsgFormat::Text => {
|
||||||
// Short plain text – send as simple text message
|
// Short plain text – send as simple text message
|
||||||
let result = self
|
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;
|
.await;
|
||||||
remove_reaction.await;
|
remove_reaction.await;
|
||||||
return result;
|
return result;
|
||||||
@ -2452,7 +2514,7 @@ impl Channel for FeishuChannel {
|
|||||||
// Medium content with links – send as rich-text post
|
// Medium content with links – send as rich-text post
|
||||||
let post_body = Self::markdown_to_post(content);
|
let post_body = Self::markdown_to_post(content);
|
||||||
let result = self
|
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;
|
.await;
|
||||||
remove_reaction.await;
|
remove_reaction.await;
|
||||||
return result;
|
return result;
|
||||||
@ -2472,11 +2534,12 @@ impl Channel for FeishuChannel {
|
|||||||
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
|
// Fallback to plain text
|
||||||
let result = self
|
let result = self
|
||||||
.send_message_to_feishu(
|
.dispatch_send(
|
||||||
receive_id,
|
receive_id,
|
||||||
receive_id_type,
|
receive_id_type,
|
||||||
"text",
|
"text",
|
||||||
content,
|
content,
|
||||||
|
reply_to,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
remove_reaction.await;
|
remove_reaction.await;
|
||||||
@ -2490,7 +2553,7 @@ impl Channel for FeishuChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !msg.content.trim().is_empty() {
|
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?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2500,21 +2563,23 @@ impl Channel for FeishuChannel {
|
|||||||
let result = match media_item.media_type.as_str() {
|
let result = match media_item.media_type.as_str() {
|
||||||
"image" => {
|
"image" => {
|
||||||
let image_key = self.upload_image(path).await?;
|
let image_key = self.upload_image(path).await?;
|
||||||
self.send_message_to_feishu(
|
self.dispatch_send(
|
||||||
receive_id,
|
receive_id,
|
||||||
receive_id_type,
|
receive_id_type,
|
||||||
"image",
|
"image",
|
||||||
&serde_json::json!({ "image_key": image_key }).to_string(),
|
&serde_json::json!({ "image_key": image_key }).to_string(),
|
||||||
|
reply_to,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
"audio" | "file" | "video" => {
|
"audio" | "file" | "video" => {
|
||||||
let file_key = self.upload_file(path).await?;
|
let file_key = self.upload_file(path).await?;
|
||||||
self.send_message_to_feishu(
|
self.dispatch_send(
|
||||||
receive_id,
|
receive_id,
|
||||||
receive_id_type,
|
receive_id_type,
|
||||||
"file",
|
"file",
|
||||||
&serde_json::json!({ "file_key": file_key }).to_string(),
|
&serde_json::json!({ "file_key": file_key }).to_string(),
|
||||||
|
reply_to,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user