From 085d7c55890835c5198ce778fd7d85d5a58d44f2 Mon Sep 17 00:00:00 2001 From: JoeAllen <2494136784@qq.com> Date: Mon, 28 Sep 2026 19:04:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E4=B8=8E=E5=8F=98=E9=87=8F=E6=B3=A8=E5=85=A5=EF=BC=88=E9=81=97?= =?UTF-8?q?=E5=98=B1=E6=A8=A1=E6=9D=BF=E6=B3=A8=E5=85=A5=E3=80=81v5=20?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=B1=9E=E6=80=A7=E6=B3=A8=E5=85=A5=E3=80=81?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=A2=84=E8=AE=BE=E4=BA=8C=E6=AC=A1=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gpui-app/src/model.rs | 55 ++++++++++++++++++++++++++++ gpui-app/src/ui/aliyun_dialog.rs | 4 ++- gpui-app/src/ui/app.rs | 5 ++- gpui-app/src/ui/connection_view.rs | 57 +++++++++++++++++++++++------- 4 files changed, 107 insertions(+), 14 deletions(-) diff --git a/gpui-app/src/model.rs b/gpui-app/src/model.rs index 64bcb9e..72232b0 100644 --- a/gpui-app/src/model.rs +++ b/gpui-app/src/model.rs @@ -526,6 +526,23 @@ pub fn render_template(input: &str, vars: &[GlobalVariable]) -> String { out } +/// 连接前渲染遗嘱消息中的 `{{变量}}`。 +/// 只作用于本次连接的配置副本,落盘的连接配置保持模板原文, +/// 这样改全局变量后下次重连即生效,`$ts` 等也按连接时刻求值。 +pub fn render_will_templates(cfg: &mut ConnectionConfig, vars: &[GlobalVariable]) { + let Some(will) = cfg.last_will.as_mut() else { + return; + }; + will.topic = render_template(&will.topic, vars); + will.payload = render_template(&will.payload, vars); + if let Some(v) = will.content_type.take() { + will.content_type = Some(render_template(&v, vars)); + } + if let Some(v) = will.response_topic.take() { + will.response_topic = Some(render_template(&v, vars)); + } +} + // ─── 日志 ──────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -729,6 +746,44 @@ mod tests { assert_eq!(render_template("中文{{$no}}", &[]), "中文{{$no}}"); } + // 连接前遗嘱模板注入:只改副本、空遗嘱不 panic、v5 属性一并渲染 + #[test] + fn render_will_templates_injects_and_keeps_no_will_safe() { + let vars = vec![GlobalVariable { + key: "device".into(), + value: "sensor-9".into(), + }]; + // 无遗嘱:直接返回 + let mut plain = ConnectionConfig::new(); + render_will_templates(&mut plain, &vars); + assert!(plain.last_will.is_none()); + + let mut cfg = ConnectionConfig::new(); + cfg.last_will = Some(LastWill { + topic: "alarm/{{device}}".into(), + payload: "d={{device}}".into(), + qos: 1, + retain: false, + content_type: Some("text/{{device}}".into()), + response_topic: Some("ack/{{device}}".into()), + }); + render_will_templates(&mut cfg, &vars); + let w = cfg.last_will.as_ref().unwrap(); + assert_eq!(w.topic, "alarm/sensor-9"); + assert_eq!(w.payload, "d=sensor-9"); + assert_eq!(w.content_type.as_deref(), Some("text/sensor-9")); + assert_eq!(w.response_topic.as_deref(), Some("ack/sensor-9")); + // 原始模板串不因注入而需要回写:连接配置持久化的是模板原文, + // 这里只验证副本注入结果;未知占位符保留 + let mut raw = ConnectionConfig::new(); + raw.last_will = Some(LastWill { + topic: "t/{{unknown}}".into(), + ..Default::default() + }); + render_will_templates(&mut raw, &vars); + assert_eq!(raw.last_will.as_ref().unwrap().topic, "t/{{unknown}}"); + } + // 预设持久化往返:raw_bytes 不落盘,参数完整保留 #[test] fn publish_preset_roundtrip_skips_raw_bytes() { diff --git a/gpui-app/src/ui/aliyun_dialog.rs b/gpui-app/src/ui/aliyun_dialog.rs index 2b30984..26099d3 100644 --- a/gpui-app/src/ui/aliyun_dialog.rs +++ b/gpui-app/src/ui/aliyun_dialog.rs @@ -15,7 +15,7 @@ use gpui_kit::{ }; use crate::aliyun::{AliyunAuthMode, AliyunPreset}; -use crate::model::ConnectionConfig; +use crate::model::{render_will_templates, ConnectionConfig}; use crate::ui::app::MqttXApp; use crate::ui::widgets::{field, make_select, OptionDelegate}; @@ -137,6 +137,8 @@ impl AliyunDialog { conn.name = preset.name.clone(); self.app.update(cx, |a, cx| { a.save_connection(conn.clone()); + // 遗嘱字段同样走 {{变量}} 注入(与其它连接入口保持一致) + render_will_templates(&mut conn, &a.variables); a.engine.connect(conn); cx.notify(); }); diff --git a/gpui-app/src/ui/app.rs b/gpui-app/src/ui/app.rs index 4584da4..aec82e8 100644 --- a/gpui-app/src/ui/app.rs +++ b/gpui-app/src/ui/app.rs @@ -487,7 +487,10 @@ impl MqttXApp { if self.engine.is_connected(&cfg.id) { self.engine.close(&cfg.id, true); } else { - self.engine.connect(cfg.clone()); + // 遗嘱支持 {{变量}}:对副本注入,保存的配置仍为模板原文 + let mut cfg = cfg.clone(); + crate::model::render_will_templates(&mut cfg, &self.variables); + self.engine.connect(cfg); } } diff --git a/gpui-app/src/ui/connection_view.rs b/gpui-app/src/ui/connection_view.rs index 9549850..dc3fbd4 100644 --- a/gpui-app/src/ui/connection_view.rs +++ b/gpui-app/src/ui/connection_view.rs @@ -24,7 +24,7 @@ use gpui_kit::{ use gpui_kit::component::IndexPath; use crate::model::{ - render_template, ConnectionConfig, ConnectionStatus, Direction, LogLevel, MqttRecord, + render_template, render_will_templates, ConnectionConfig, ConnectionStatus, Direction, LogLevel, MqttRecord, PayloadFormat, PublishParams, SubscribeOptions, Subscription, }; use crate::mqtt::MqttEngine; @@ -588,6 +588,10 @@ impl ConnectionView { .into_iter() .map(|(k, v)| (render_template(&k, &vars), render_template(&v, &vars))) .collect(); + // v5 文本属性(Content-Type / Response Topic / Correlation Data)一并注入 + params.content_type = params.content_type.map(|v| render_template(&v, &vars)); + params.response_topic = params.response_topic.map(|v| render_template(&v, &vars)); + params.correlation_data = params.correlation_data.map(|v| render_template(&v, &vars)); if let PayloadFormat::Json = params.payload_format && let Err(e) = serde_json::from_str::(¶ms.payload) { @@ -794,10 +798,15 @@ impl ConnectionView { return; } let cfg = this.config(cx); - if let Some(cfg) = cfg { + if let Some(mut cfg) = cfg { if this.engine.is_connected(&cfg.id) { this.engine.close(&cfg.id, true); } else { + // 遗嘱 {{变量}} 在每次连接时按当前全局变量求值 + let vars = this + .with_app(cx, |app| app.variables.clone()) + .unwrap_or_default(); + render_will_templates(&mut cfg, &vars); this.engine.connect(cfg); } } @@ -2050,20 +2059,44 @@ impl ConnectionView { for p in &del_presets { let weak2 = weak_del.clone(); let id = p.id.clone(); + let name = p.name.clone(); let label = format!("删除「{}」", p.name); submenu = submenu.item( PopupMenuItem::new(SharedString::from(label)).on_click( - move |_ev, _w, cx| { - if let Some(view) = weak2.upgrade() { - view.update(cx, |view, cx| { - if let Some(app) = view.app.upgrade() { - app.update(cx, |app, cx| { - app.delete_publish_preset(&id); + move |_ev, window, cx| { + // 预设删除不可撤销,先弹二次确认 + let weak = weak2.clone(); + let pid = id.clone(); + let pname = name.clone(); + window.open_alert_dialog(cx, move |alert, _, _| { + let weak = weak.clone(); + let pid = pid.clone(); + alert + .title("删除预设") + .description(format!( + "确定删除预设「{pname}」吗?此操作不可撤销。" + )) + .button_props( + DialogButtonProps::default() + .show_cancel(true) + .cancel_text("取消") + .ok_text("删除") + .ok_variant(ButtonVariant::Danger), + ) + .on_ok(move |_, _, cx| { + weak.update(cx, |view, cx| { + if let Some(app) = view.app.upgrade() { + app.update(cx, |app, cx| { + app.delete_publish_preset(&pid); + cx.notify(); + }); + } cx.notify(); - }); - } - }); - } + }) + .ok(); + true + }) + }); }, ), );