新增: 挂机模式 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:
lxy
2026-10-07 22:03:41 +08:00
parent e60bd38444
commit 2441101978
16 changed files with 525 additions and 264 deletions
+17 -15
View File
@@ -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;
}
+58
View File
@@ -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 覆盖标志 + 看板 🔒 行,写入执行闸静态位。
+3
View File
@@ -508,6 +508,9 @@ pub fn run() {
commands::ai::ai_conversation_list,
commands::ai::ai_list_recoverable,
commands::ai::ai_recover_parked,
// 挂机引擎(无人值守):状态查询 + 模式开关(设置页挂机区块/TopBar 徽章)
commands::ai::unattended::unattended_status,
commands::ai::unattended::unattended_set_enabled,
commands::ai::ai_conversation_switch,
// S3 子代理轨迹只读窥视(复用 switch 读路径,不切任何会话状态)
commands::ai::ai_conversation_peek,