新增: 挂机模式 P1 总装: 设置面板+顶栏徽章+边界卡精简+监控告警推送
- 新增 UnattendedPanel.vue 挂机配置面板(引擎自动认领看板任务,Settings KV 热读即改即生效) - Settings.vue 接入挂机面板;searchIndex.ts 补挂机搜索词条 - TopBar.vue 新增挂机进行中徽章(脉冲动效+透明度过渡) - TaskBoundaryCard.vue 精简边界状态判定(wallState 合并比对,删冗余 watch) - i18n zh-CN/en: settings/ai/aiChat 六文件补挂机词条双语 - unattended.rs 新增 push_monitor_alert 监控告警推送(异常快照+告警头直发 monitor_chat) - feishu_notify.rs push/push_card 出口重构收敛 - lib.rs 注册 unattended_status/unattended_set_enabled 两 IPC - aiShared.ts/useAiStreamingEvents.ts 挂机状态流接入
This commit is contained in:
@@ -1,33 +1,35 @@
|
||||
//! 飞书告警出口(todo 37 监控轮询):挂机引擎自监控快照的推送通道。
|
||||
//! 飞书推送出口(todo 37 监控轮询 + 挂机开关确认卡)。
|
||||
//!
|
||||
//! 职责单一:把 unattended.rs run_self_monitor 的异常快照送达指定飞书群。
|
||||
//! 职责单一:把挂机引擎的异常快照与开关确认消息送达指定飞书群。
|
||||
//! 复用 feishu_channel 的卡片发送链路(md_to_lark_friendly 净化 + markdown 卡),
|
||||
//! 目标 chat 显式传入(KV df-monitor-chat-id),不走 conv 绑定路由——监控告警
|
||||
//! 是运维通道,与会话投影(卡片滚动句柄/终态判据)互不污染。
|
||||
//!
|
||||
//! 失败语义:告警是锦上添花,发送失败只落 feishu_log 不 panic 不重试——
|
||||
//! 失败语义:推送是锦上添花,发送失败只落 feishu_log 不 panic 不重试——
|
||||
//! 下一监控周期同段异常仍会再次尝试(同段只告警一次的闸在调用方)。
|
||||
|
||||
use crate::feishu_channel::{feishu_log, send_markdown_card};
|
||||
|
||||
/// 监控告警推送:快照文本直发 monitor_chat(飞书 markdown 卡)。
|
||||
/// 渠道未启动/发送失败均静默降级为 feishu_log 落盘(告警不阻塞引擎主循环)。
|
||||
pub(crate) async fn push_monitor_alert(app: &tauri::AppHandle, chat_id: &str, snapshot: &str) {
|
||||
let _ = app; // AppHandle 预留(将来查 DB 富化告警),当前快照自包含
|
||||
/// 通用推卡:文本直发指定 chat_id(飞书 markdown 卡)。
|
||||
/// 未配目标/渠道未启动/发送失败均降级为 feishu_log 落盘,不阻塞调用方主流程。
|
||||
pub(crate) async fn push_card(_app: &tauri::AppHandle, chat_id: &str, text: &str) {
|
||||
if chat_id.is_empty() {
|
||||
feishu_log(&format!("[monitor] 未配置 df-monitor-chat-id,告警仅落日志: {snapshot}"));
|
||||
feishu_log(&format!("[notify] 未配置 df-monitor-chat-id,仅落日志: {text}"));
|
||||
return;
|
||||
}
|
||||
let Some(client) = crate::feishu_channel::active_client() else {
|
||||
feishu_log(&format!(
|
||||
"[monitor] 飞书渠道未启动,告警仅落日志: {snapshot}"
|
||||
));
|
||||
feishu_log(&format!("[notify] 飞书渠道未启动,仅落日志: {text}"));
|
||||
return;
|
||||
};
|
||||
let text = format!("🛡 **DevFlow 挂机监控告警**\n{snapshot}");
|
||||
let sent = send_markdown_card(client, "", chat_id, &text).await;
|
||||
let sent = send_markdown_card(client, "", chat_id, text).await;
|
||||
match sent {
|
||||
Some(mid) => feishu_log(&format!("[monitor] 告警已推送 chat={chat_id} card={mid}")),
|
||||
None => feishu_log(&format!("[monitor] 告警推送失败(卡片与文本均不可达),快照: {snapshot}")),
|
||||
Some(mid) => feishu_log(&format!("[notify] 已推送 chat={chat_id} card={mid}")),
|
||||
None => feishu_log(&format!("[notify] 推送失败(卡片与文本均不可达),原文: {text}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 监控告警推送:异常快照加告警头直发 monitor_chat。
|
||||
pub(crate) async fn push_monitor_alert(app: &tauri::AppHandle, chat_id: &str, snapshot: &str) {
|
||||
let text = format!("🛡 **DevFlow 挂机监控告警**\n{snapshot}");
|
||||
push_card(app, chat_id, &text).await;
|
||||
}
|
||||
|
||||
@@ -494,6 +494,64 @@ async fn run_self_monitor(app: &AppHandle, cfg: &EngineConfig) {
|
||||
.await;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC:引擎状态查询 + 模式开关(设置页挂机区块 / TopBar 徽章消费)
|
||||
// ============================================================
|
||||
|
||||
/// 引擎状态快照(KV 配置 + 引擎静态状态,纯读零副作用)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct UnattendedStatus {
|
||||
enabled: bool,
|
||||
claim_scope: String,
|
||||
in_flight_conv: Option<String>,
|
||||
breaker_tripped: bool,
|
||||
poll_secs: u64,
|
||||
}
|
||||
|
||||
/// 查询挂机引擎当前状态(设置页挂机区块回显 / TopBar 徽章轮询)
|
||||
#[tauri::command]
|
||||
pub async fn unattended_status(app: AppHandle) -> Result<UnattendedStatus, String> {
|
||||
let cfg = read_config(&app).await;
|
||||
let e = engine();
|
||||
let in_flight = e
|
||||
.in_flight
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.clone();
|
||||
Ok(UnattendedStatus {
|
||||
enabled: cfg.enabled,
|
||||
claim_scope: match cfg.claim_scope {
|
||||
ClaimScope::RedOnly => "red_only".to_string(),
|
||||
ClaimScope::AllOpen => "all_open".to_string(),
|
||||
},
|
||||
in_flight_conv: in_flight,
|
||||
breaker_tripped: e.breaker_tripped.load(Ordering::SeqCst),
|
||||
poll_secs: cfg.poll_secs,
|
||||
})
|
||||
}
|
||||
|
||||
/// 挂机模式开关:写 KV(引擎每轮热读)+ 唤醒轮询器即时生效 + 经飞书推确认卡。
|
||||
/// 关闭语义 = 引擎空转(下一轮只读配置即停),不动在途任务。
|
||||
#[tauri::command]
|
||||
pub async fn unattended_set_enabled(app: AppHandle, enabled: bool) -> Result<bool, String> {
|
||||
let state = app.state::<AppState>();
|
||||
state
|
||||
.settings
|
||||
.set(KV_ENABLED, if enabled { "true" } else { "false" })
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
// 唤醒轮询器:开=不等当前周期走完立即扫板(领单/恢复 parked),关=立即空转并复位熔断
|
||||
engine().notify.notify_one();
|
||||
let chat = read_config(&app).await.monitor_chat;
|
||||
let text = if enabled {
|
||||
"🛌 挂机模式已开启:将自动认领看板高危任务"
|
||||
} else {
|
||||
"挂机模式已关闭"
|
||||
};
|
||||
crate::commands::ai::feishu_notify::push_card(&app, chat.as_deref().unwrap_or(""), text).await;
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
const RECOVER_NONE: &str = "无可恢复 parked 会话";
|
||||
|
||||
/// 提交冻结标志同步(39 治理):读 KV 覆盖标志 + 看板 🔒 行,写入执行闸静态位。
|
||||
|
||||
Reference in New Issue
Block a user