From 45f0560f85b7d4ecf811342958e88dd8909075c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sat, 17 Oct 2026 22:19:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E9=A3=9E=E4=B9=A6?= =?UTF-8?q?=E9=95=BF=E8=BF=9E=E6=8E=A5=E5=81=87=E6=B4=BB=E8=87=AA=E6=84=88?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=8E=A2=E6=B4=BB=E6=8C=81=E7=BB=AD=E6=96=AD?= =?UTF-8?q?=E6=B5=81=E5=90=8E=E4=B8=BB=E5=8A=A8=E6=96=AD=E5=BC=80=E9=87=8D?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 看门狗连续 3 个巡检周期(断流约13分钟)判定假活后 Notify 唤醒 run_loop 主动退出,复用外层 backoff 重连循环重建全新 WS 握手 只告警不自愈的盲区已补齐,比 SDK 内部重连更彻底(全新握手) --- scripts/check-running-version.ps1 | 10 ++++++ scripts/check-updater-keypair.ps1 | 33 ++++++++++++++++++++ src-tauri/src/feishu_channel.rs | 52 ++++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 scripts/check-running-version.ps1 create mode 100644 scripts/check-updater-keypair.ps1 diff --git a/scripts/check-running-version.ps1 b/scripts/check-running-version.ps1 new file mode 100644 index 0000000..ee06059 --- /dev/null +++ b/scripts/check-running-version.ps1 @@ -0,0 +1,10 @@ +# 探测本机正在运行的 DevFlow 进程及其版本(只读) +$p = Get-Process devflow -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path +if ($p) { + $v = (Get-Item $p).VersionInfo + Write-Host "PATH=$p" + Write-Host "FileVersion=$($v.FileVersion)" + Write-Host "ProductVersion=$($v.ProductVersion)" +} else { + Write-Host "NO_DEVFLOW_PROCESS" +} diff --git a/scripts/check-updater-keypair.ps1 b/scripts/check-updater-keypair.ps1 new file mode 100644 index 0000000..8660b93 --- /dev/null +++ b/scripts/check-updater-keypair.ps1 @@ -0,0 +1,33 @@ +# 校验 updater 验签密钥配对(终版): 比对 keyid(KEYNUM 8字节) —— minisign 格式对齐 +# conf.pubkey blob(42B): SIGALG"Ed"[0..2] + KEYNUM[2..10] + PUBKEY[10..42] +# 私钥文件 blob(116B): SIGALG"Rs"[0..2] + KDFALG[2..4] + CKSUM[4..12] + SALT[12..28] + OPS[28..36] + MEM[36..44] + KEYNUM[44..52] + SIG[52..116] +$confPath = "E:\wk-lab\devflow\src-tauri\tauri.conf.json" +$keyPath = Join-Path $env:USERPROFILE ".tauri\devflow.key" + +# 1. conf 侧: 提取 pubkey → 解码 → 取 KEYNUM +$raw = Get-Content $confPath -Raw +if ($raw -match '"pubkey"\s*:\s*"([A-Za-z0-9+/=]{40,400})"') { $confPubB64 = $Matches[1] } else { Write-Host "CONF_PUBKEY_NOT_FOUND"; exit 1 } +# conf.pubkey 是 base64(minisign .pub 文件文本),需二次解码取末行 +$confText = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($confPubB64)) +$pubLines = @($confText -split "`n" | Where-Object { $_.Trim() -and $_ -notmatch 'comment' } | ForEach-Object { $_.Trim() }) +if ($pubLines.Count -lt 1) { Write-Host "CONF_PUB_FORMAT_UNEXPECTED"; exit 1 } +$confBlob = [Convert]::FromBase64String($pubLines[0]) +$confKeynum = ($confBlob[2..9] | ForEach-Object { $_.ToString("x2") }) -join "" +Write-Host "conf keyid : $confKeynum (blob $($confBlob.Length)B)" + +# 2. key 侧: 私钥文件 = 注释行 + 1 条 b64 blob(KEYNUM 未加密,明文可读) +if (-not (Test-Path $keyPath)) { Write-Host "KEY_NOT_FOUND: $keyPath"; exit 1 } +$b64Lines = @(Get-Content $keyPath | Where-Object { $_ -and $_ -notmatch 'comment' } | ForEach-Object { $_.Trim() }) +if ($b64Lines.Count -lt 1) { Write-Host "KEY_FORMAT_UNEXPECTED"; exit 1 } +$secBlob = [Convert]::FromBase64String($b64Lines[0]) +$secKeynum = ($secBlob[44..51] | ForEach-Object { $_.ToString("x2") }) -join "" +Write-Host "key keyid : $secKeynum (blob $($secBlob.Length)B)" + +# 3. 配对判定 +if ($confKeynum -eq $secKeynum) { + Write-Host "MATCH - updater 验签密钥配对正确(keyid $confKeynum), B3 安装验签可通过" + exit 0 +} else { + Write-Host "MISMATCH - tauri.conf.json 的 pubkey 与签名私钥不配对, B3 安装验签将失败!" + exit 1 +} diff --git a/src-tauri/src/feishu_channel.rs b/src-tauri/src/feishu_channel.rs index d85acac..66aad2d 100644 --- a/src-tauri/src/feishu_channel.rs +++ b/src-tauri/src/feishu_channel.rs @@ -125,6 +125,9 @@ struct Ctx { /// D3 在途会话数:消息受理(handle_message 白名单通过)到任务投递完成期间 >0, /// 看门狗要求为 0 才告警,防有回复待投递时误报 inflight: Arc, + /// D5 假活自愈唤醒句柄:看门狗连续告警超阈值后 notify,run_loop 的 channel.start() + /// select 到即主动退出,交给外层重连循环重建连接(Notify 生命周期同 Ctx,跨重连复用) + selfheal: Arc, } /// 启动入口:lib.rs setup 调用。未启用/配置缺失/构造失败一律记日志返回,不阻塞应用启动。 @@ -182,6 +185,7 @@ pub fn start_if_enabled(app: tauri::AppHandle) { debounce: Arc::new(tokio::sync::Mutex::new(HashMap::new())), last_alive: Arc::new(AtomicI64::new(now_secs())), inflight: Arc::new(AtomicUsize::new(0)), + selfheal: Arc::new(tokio::sync::Notify::new()), }); // D1 僵尸资源修复:client 的 'static 化只做一次(原在 run_loop 内,每次重连都 // Box::leak 一份 LarkClient,频繁断线场景内存阶梯上涨);Channel::builder 仅借用 @@ -285,10 +289,16 @@ let ctx_msg = ctx.clone(); }) .build(); tracing::info!("[feishu] 长连接启动(桌面端直连,免公网回调)"); - let result = channel - .start() - .await - .map_err(|e: LarkError| anyhow::anyhow!("飞书长连接退出: {e}")); + // D5 假活自愈:看门狗持续断流判定后 notify,主动终结本次 channel.start(), + // 交给外层重连循环 backoff 重建连接(比 SDK 内部重连更彻底:全新 WS 握手) + let heal = ctx.selfheal.clone(); + let result = tokio::select! { + r = channel.start() => r + .map_err(|e: LarkError| anyhow::anyhow!("飞书长连接退出: {e}")), + _ = heal.notified() => { + Err(anyhow::anyhow!("假活自愈: 看门狗判定持续断流,主动断开重建")) + } + }; watchdog.abort(); result?; Ok(()) @@ -310,25 +320,45 @@ const LIVENESS_TICK: std::time::Duration = std::time::Duration::from_secs(60); /// 排查起点(此前 bot 静默死无任何日志可查)。 async fn run_liveness_watchdog(ctx: Arc) { let mut warned = false; + // D5 假活自愈:连续告警计数。每 60s 巡检一次,断流持续 LIVENESS_HEAL_TICKS 次 + // (即断流 ≥ 600s + 3×60s 确认期)仍无任何事件恢复,判定为协议假活(SDK 读空闲 + // 自愈覆盖不了),主动 notify 唤醒 run_loop 退出重建连接,复用外层 backoff 重连 + let mut stale_ticks: u32 = 0; loop { tokio::time::sleep(LIVENESS_TICK).await; let elapsed = now_secs() - ctx.last_alive.load(Ordering::SeqCst); if elapsed <= LIVENESS_STALE_SECS { warned = false; + stale_ticks = 0; continue; } - // 有会话在途不告警(回复投递依赖当前连接);同一段断流只告警一次防刷屏 - if ctx.inflight.load(Ordering::SeqCst) > 0 || warned { + // 有会话在途不告警(回复投递依赖当前连接) + if ctx.inflight.load(Ordering::SeqCst) > 0 { continue; } - warned = true; - tracing::warn!("[feishu] 探活告警: {elapsed}s 无任何事件且无在途会话,疑似连接假活"); - feishu_log(&format!( - "探活告警: {elapsed}s 无任何事件且无在途会话(疑似假活,持续出现请重启应用重建连接)" - )); + if !warned { + warned = true; + tracing::warn!("[feishu] 探活告警: {elapsed}s 无任何事件且无在途会话,疑似连接假活"); + feishu_log(&format!( + "探活告警: {elapsed}s 无任何事件且无在途会话(疑似假活)" + )); + } + stale_ticks += 1; + if stale_ticks >= LIVENESS_HEAL_TICKS { + tracing::error!( + "[feishu] 假活自愈触发: 断流持续 {} 个巡检周期,主动断开重建长连接", + stale_ticks + ); + feishu_log("假活自愈触发: 持续断流,主动断开重建长连接"); + ctx.selfheal.notify_waiters(); + return; + } } } +/// D5 自愈确认期:首个告警后再等 N 个巡检周期仍断流才重建(防把正常空闲+慢恢复误杀) +const LIVENESS_HEAL_TICKS: u32 = 3; + /// D3 在途会话计数 RAII:handle_message 从注册等待桥到投递完成的任意退出路径 /// (spawn 失败早退/投递完成)都保证计数回落 struct InflightGuard(Arc);