新增: 飞书 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
+1 -1
View File
@@ -1053,7 +1053,7 @@ graph TD
### ⚠️ 退回重处理(2026-10-06 用户令,G2/G3 治理标准首次执行)
- 三笔提交(cec4d11 委托引擎/84df9b5 租约/622507c UI)已 reset --mixed 退回(内容全在工作区,零丢失,未推远端无需处理远端)
- **重做要求**:①先修 🔴10 loop_state 失效(G2 门禁:🔴不清零不得提交新功能)②描述按 G3 如实:实际干了 14+16+事件广播三件须逐件列全 ③i18n reattachToMain 键随批补上 ④修完按主题重新分组提交,销账 [x] 附验收输出(G4)
19. [ ] **飞书 outbound 统一出口**(11:23 实证):挂机模式进度汇总走独立发送路径,绕过 downgrade_md_tables/clip_for_card/卡片链——3 条回复带完整 md 表格未降级(踩回表格渲染不稳坑)。修:降级/裁剪/卡片链下沉为统一 outbound 出口函数,对话/挂机汇报/进度推送全部走它;验收:任意路径回复无 |---| 裸奔。
19. [x] **飞书 outbound 统一出口**(11:23 实证)✅ 2026-10-06 收口:勘察确认对话/受理/完成/注入回执已统一走 deliver_result(降级/裁剪/卡片链内聚);病灶「挂机进度汇总独立发送路径」在当前 main 不存在(属退回的挂机实验代码)。本批防复发收口:①新增 push_to_chat pub(crate) 正典出口(主动推送形态——挂机进度汇总/巡检通知将来落地时禁再开独立路径,强制走降级/裁剪/卡片链) ②ACTIVE_CLIENT OnceLock 全局句柄(渠道启动就绪,推送复用长连接 client 不临时建连,未启动静默跳过)。验收:cargo check EXIT 0 + feishu 7 passed。
> 附:挂机汇报自报 🔴10 已修验证通过+43 文件产出——挂机模式推进有效,路径修复别误伤它。
## 无人值守可靠性专项(2026-10-06 swarm 立项,设计档:docs/02-架构设计/无人值守可靠性专项设计-20261006.md+temp/V49断点续跑设计-20261006.md)
+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 {