新增: 飞书 push_to_chat outbound 统一出口+ACTIVE_CLIENT 全局句柄(19)

This commit is contained in:
lxy
2026-10-06 12:49:48 +08:00
parent d2f586fc52
commit 40392ebded
2 changed files with 33 additions and 1 deletions
+32
View File
@@ -26,6 +26,11 @@ const KV_FEISHU_CONFIG: &str = "feishu-config";
/// app_settings 键:飞书 chat_id → conversation_id 映射(持久化,跨重启续会话)
const KV_CHAT_MAP: &str = "feishu-chat-map";
/// 19 outbound 统一出口:渠道启动成功后的全局 client 句柄(once 填充,渠道停用为空)。
/// push_to_chat 主动推送复用长连接 client,不临时建连;进程生命周期内渠道只启一次,
/// OnceLock 语义匹配(重启应用才可能重配)。
static ACTIVE_CLIENT: std::sync::OnceLock<&'static LarkClient> = std::sync::OnceLock::new();
#[derive(Debug, Clone, Deserialize)]
struct FeishuConfig {
#[serde(default)]
@@ -120,6 +125,8 @@ pub fn start_if_enabled(app: tauri::AppHandle) {
// Box::leak 一份 LarkClient,频繁断线场景内存阶梯上涨);Channel::builder 仅借用
// 引用,重连循环内复用同一 leaked 引用,只重建 Channel/dispatcher
let leaked: &'static LarkClient = Box::leak(Box::new(client));
// 19 outbound 统一出口:全局句柄就绪,push_to_chat 自此可用
let _ = ACTIVE_CLIENT.set(leaked);
// 断线自动重连:网络波动/服务端断开导致 start 退出时 backoff 重试,防 bot 静默死
let mut backoff = 2u64;
loop {
@@ -766,6 +773,31 @@ async fn deliver_result(
send_markdown_card(client, message_id, chat_id, &text).await;
}
/// 19 outbound 统一出口:主动推送(无占位卡上下文的外部推送方——挂机进度汇总/巡检通知
/// 等将来形态)。降级/裁剪/卡片链与 deliver_result 同源(禁再开独立发送路径绕过降级链,
/// 11:23 实证:绕过链的回复带完整 md 表格未降级,踩回表格渲染不稳坑)。
/// 推送目标:chat_map 最近映射的会话;渠道未启动(无 client)或无映射时静默跳过。
pub(crate) async fn push_to_chat(app: &tauri::AppHandle, text: &str) {
let Some(client) = ACTIVE_CLIENT.get() else {
return; // 渠道未启动:推送无落点,静默跳过(调用方自行决定是否记日志)
};
let db = app.state::<crate::state::AppState>().db.clone();
let repo = SettingsRepo::new(&db);
let raw = repo.get(KV_CHAT_MAP).await.ok().flatten();
let Some(map) = raw.and_then(|r| serde_json::from_str::<HashMap<String, String>>(&norm(Some(r))).ok())
else {
return;
};
// 最近会话:取任一映射(KV HashMap 无序,取首条;多 chat 精准路由属将来形态)
let Some(chat_id) = map.keys().next().cloned() else {
return;
};
let processed = clip_for_card(&downgrade_md_tables(text), 8000);
// 主动推送无 message_id 上下文:send_markdown_card 的 reply 分支跳过,直接 chat 卡片
send_markdown_card(client, "", &chat_id, &processed).await;
feishu_log(&format!("push_to_chat chat={chat_id} len={}", processed.len()));
}
/// 卡片正文长度防御:飞书卡片 JSON 约 30KB 上限,超长会让原地更新与新发双双失败,
/// 一路穿透到纯文本兜底仍不可读;入口统一裁剪,尾注指回桌面端(u-claw archiveIfClipped 同口径)
fn clip_for_card(text: &str, max_chars: usize) -> String {