fix(i18n): MQTT 引擎与阿里云模块用户可见文案接入语言包

This commit is contained in:
2026-10-14 18:31:41 +08:00
parent 5b8edede46
commit 60501b6465
7 changed files with 267 additions and 99 deletions
+57
View File
@@ -4,14 +4,18 @@
"aliyun.auth_token_mode": "Group token" "aliyun.auth_token_mode": "Group token"
"aliyun.auth_triple_mode": "Per-device secret" "aliyun.auth_triple_mode": "Per-device secret"
"aliyun.build_failed": "Failed to build connection: {e}" "aliyun.build_failed": "Failed to build connection: {e}"
"aliyun.default_preset_name": "Alibaba Cloud device"
"aliyun.device_id": "Device ID" "aliyun.device_id": "Device ID"
"aliyun.device_id_required": "Device ID cannot be empty"
"aliyun.device_secret": "DeviceSecret (per-device)" "aliyun.device_secret": "DeviceSecret (per-device)"
"aliyun.device_secret_ph": "Triple: DeviceSecret" "aliyun.device_secret_ph": "Triple: DeviceSecret"
"aliyun.generate_connect": "Generate & connect" "aliyun.generate_connect": "Generate & connect"
"aliyun.group": "Group (optional)" "aliyun.group": "Group (optional)"
"aliyun.group_id": "Group ID" "aliyun.group_id": "Group ID"
"aliyun.help": "Token mode (Alibaba Cloud MQTT): endpoint {实例ID}.mqtt.aliyuncs.com, clientId={GroupId}@@@{设备ID}, password is the HMAC-SHA1 signature. Per-device secret (IoT Platform): endpoint {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com, signed per the official triple spec." "aliyun.help": "Token mode (Alibaba Cloud MQTT): endpoint {实例ID}.mqtt.aliyuncs.com, clientId={GroupId}@@@{设备ID}, password is the HMAC-SHA1 signature. Per-device secret (IoT Platform): endpoint {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com, signed per the official triple spec."
"aliyun.hmac_init_failed": "HMAC init failed: {e}"
"aliyun.instance_id": "Instance ID" "aliyun.instance_id": "Instance ID"
"aliyun.instance_id_required": "Instance ID cannot be empty"
"aliyun.preset_name": "Preset name" "aliyun.preset_name": "Preset name"
"aliyun.product_key": "ProductKey (per-device)" "aliyun.product_key": "ProductKey (per-device)"
"aliyun.product_key_ph": "Triple: ProductKey" "aliyun.product_key_ph": "Triple: ProductKey"
@@ -20,6 +24,9 @@
"aliyun.save_preset": "Save preset" "aliyun.save_preset": "Save preset"
"aliyun.saved_presets": "Saved presets" "aliyun.saved_presets": "Saved presets"
"aliyun.title": "Alibaba Cloud IoT" "aliyun.title": "Alibaba Cloud IoT"
"aliyun.token_required_fields": "Group ID, AccessKey ID and AccessKey Secret cannot be empty"
"aliyun.triple_requires_device_name": "Per-device secret mode requires DeviceName"
"aliyun.triple_requires_pk_ds": "Per-device secret mode requires ProductKey and DeviceSecret"
"color.blue": "Blue" "color.blue": "Blue"
"color.cyan": "Cyan" "color.cyan": "Cyan"
"color.green": "Green" "color.green": "Green"
@@ -57,6 +64,43 @@
"encode.hex_failed": "Hex decode failed: {e}" "encode.hex_failed": "Hex decode failed: {e}"
"encode.hex_invalid": "Hex decode failed: non-hex character present" "encode.hex_invalid": "Hex decode failed: non-hex character present"
"encode.hex_odd": "Hex length must be even (two chars per byte)" "encode.hex_odd": "Hex length must be even (two chars per byte)"
"engine.cancelled": "Connection cancelled"
"engine.connack_rejected": "Connection rejected: {code}"
"engine.connack_rejected_code": "Connection rejected: {code} (code {n})"
"engine.connect_failed": "Connect failed: {e}"
"engine.connect_ok": "Connected (session_present: {sp})"
"engine.connecting": "Connecting to {host}:{port} ({protocol}, {transport})"
"engine.connection_error": "Connection error: {e}"
"engine.connection_not_found": "Connection not found"
"engine.disconnect_received": "DISCONNECT received: {code} (code {n})"
"engine.disconnected_by_server": "Disconnected by server"
"engine.disconnected_by_server_active": "Server closed the connection"
"engine.disconnected_by_server_code": "Disconnected by server: {code}"
"engine.events_dropped": "UI cannot keep up: dropped {n} high-frequency events (messages/logs) so far"
"engine.internal_task_failed": "Internal task failed: {e}"
"engine.puback": "Publish acknowledged (packet {n})"
"engine.puback_reason": "Publish acknowledged (packet {n}, reason {reason})"
"engine.pubcomp": "QoS2 publish complete (packet {n})"
"engine.publish_failed": "Publish failed: {e}"
"engine.publish_no_connection": "Publish failed: connection not found"
"engine.publish_outgoing": "Message sent (packet {n})"
"engine.publish_sent": "Published to {topic} (QoS {qos}, retain={retain})"
"engine.pubrec": "QoS2 publish received (packet {n})"
"engine.received_message": "Received: {topic} ({n} bytes)"
"engine.received_message_qos": "Received: {topic} ({n} bytes, QoS {qos})"
"engine.reconnect_give_up": "Reconnect failed {attempts} times, reached the max of {max}, giving up: {e}"
"engine.reconnect_retry": "Connection error, retrying in {delay}s (attempt {attempts}): {e}"
"engine.suback": "Subscribe confirmed (packet {n})"
"engine.suback_codes": "Subscribe confirmed (packet {n}): {codes}"
"engine.subscribe_rejected": "Subscribe rejected: {e}"
"engine.subscribe_request_failed": "Subscribe request failed: {e}"
"engine.subscribe_topic": "Subscribing: {topic} (QoS {qos})"
"engine.test_timeout": "Connection test timed out ({secs}s) — check the host and port"
"engine.unsuback": "Unsubscribe confirmed"
"engine.unsuback_packet": "Unsubscribe confirmed (packet {n})"
"engine.unsubscribe_failed": "Unsubscribe failed: {e}"
"engine.unsubscribe_ok": "Unsubscribed: {topic}"
"engine.user_disconnect": "Disconnected by user request"
"form.auth_section": "Auth & Keep-alive" "form.auth_section": "Auth & Keep-alive"
"form.auto_connect": "Connect on app start" "form.auto_connect": "Connect on app start"
"form.auto_reconnect": "Auto reconnect on disconnect" "form.auto_reconnect": "Auto reconnect on disconnect"
@@ -312,6 +356,19 @@
"titlebar.switch_light": "Switch to light theme" "titlebar.switch_light": "Switch to light theme"
"titlebar.unpin": "Unpin window" "titlebar.unpin": "Unpin window"
"titlebar.variables": "Global Variables" "titlebar.variables": "Global Variables"
"tls.ca_invalid_cert": "Invalid certificate in CA file {p}: {e}"
"tls.ca_no_certs": "No certificate found in CA file {p}"
"tls.ca_parse_failed": "Failed to parse CA file {p}: {e}"
"tls.ca_read_failed": "Failed to read CA file {p}: {e}"
"tls.cert_key_pair_required": "Client certificate and client key must be configured together"
"tls.client_auth_failed": "Failed to load client certificate/key: {e}"
"tls.client_cert_no_certs": "No certificate found in client certificate file {p}"
"tls.client_cert_parse_failed": "Failed to parse client certificate file {p}: {e}"
"tls.client_cert_read_failed": "Failed to read client certificate file {p}: {e}"
"tls.client_key_parse_failed": "Failed to parse client key file {p}: {e}"
"tls.client_key_read_failed": "Failed to read client key file {p}: {e}"
"tls.init_failed": "TLS init failed: {e}"
"tls.native_roots_failed": "Failed to load system root certificates: {detail}"
"update.apply": "Update to v{v} now" "update.apply": "Update to v{v} now"
"update.check": "Check for updates" "update.check": "Check for updates"
"update.check_hint": "Check GitHub Releases for a new version" "update.check_hint": "Check GitHub Releases for a new version"
+57
View File
@@ -4,14 +4,18 @@
"aliyun.auth_token_mode": "Group 令牌 (Token)" "aliyun.auth_token_mode": "Group 令牌 (Token)"
"aliyun.auth_triple_mode": "一机一密" "aliyun.auth_triple_mode": "一机一密"
"aliyun.build_failed": "无法生成连接: {e}" "aliyun.build_failed": "无法生成连接: {e}"
"aliyun.default_preset_name": "阿里云设备"
"aliyun.device_id": "设备 ID" "aliyun.device_id": "设备 ID"
"aliyun.device_id_required": "Device ID 不能为空"
"aliyun.device_secret": "DeviceSecret(一机一密)" "aliyun.device_secret": "DeviceSecret(一机一密)"
"aliyun.device_secret_ph": "一机一密:DeviceSecret" "aliyun.device_secret_ph": "一机一密:DeviceSecret"
"aliyun.generate_connect": "生成并连接" "aliyun.generate_connect": "生成并连接"
"aliyun.group": "分组(可选)" "aliyun.group": "分组(可选)"
"aliyun.group_id": "Group ID" "aliyun.group_id": "Group ID"
"aliyun.help": "Token 模式(云消息队列 MQTT 版):接入点 {实例ID}.mqtt.aliyuncs.com,clientId={GroupId}@@@{设备ID},密码为 HMAC-SHA1 签名;一机一密(物联网平台):接入点 {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com,按官方三元组规范签名" "aliyun.help": "Token 模式(云消息队列 MQTT 版):接入点 {实例ID}.mqtt.aliyuncs.com,clientId={GroupId}@@@{设备ID},密码为 HMAC-SHA1 签名;一机一密(物联网平台):接入点 {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com,按官方三元组规范签名"
"aliyun.hmac_init_failed": "HMAC 初始化失败: {e}"
"aliyun.instance_id": "实例 ID" "aliyun.instance_id": "实例 ID"
"aliyun.instance_id_required": "Instance ID 不能为空"
"aliyun.preset_name": "预设名称" "aliyun.preset_name": "预设名称"
"aliyun.product_key": "ProductKey(一机一密)" "aliyun.product_key": "ProductKey(一机一密)"
"aliyun.product_key_ph": "一机一密:ProductKey" "aliyun.product_key_ph": "一机一密:ProductKey"
@@ -20,6 +24,9 @@
"aliyun.save_preset": "保存预设" "aliyun.save_preset": "保存预设"
"aliyun.saved_presets": "已保存预设" "aliyun.saved_presets": "已保存预设"
"aliyun.title": "阿里云 IoT 设备" "aliyun.title": "阿里云 IoT 设备"
"aliyun.token_required_fields": "Group ID、AccessKey ID 与 AccessKey Secret 不能为空"
"aliyun.triple_requires_device_name": "一机一密需要 DeviceName(设备名)"
"aliyun.triple_requires_pk_ds": "一机一密需要 ProductKey 与 DeviceSecret (设备密钥)"
"color.blue": "蓝" "color.blue": "蓝"
"color.cyan": "青" "color.cyan": "青"
"color.green": "绿" "color.green": "绿"
@@ -57,6 +64,43 @@
"encode.hex_failed": "Hex 解码失败: {e}" "encode.hex_failed": "Hex 解码失败: {e}"
"encode.hex_invalid": "Hex 解码失败: 含非十六进制字符" "encode.hex_invalid": "Hex 解码失败: 含非十六进制字符"
"encode.hex_odd": "Hex 长度必须为偶数(每两个字符一个字节)" "encode.hex_odd": "Hex 长度必须为偶数(每两个字符一个字节)"
"engine.cancelled": "连接已取消"
"engine.connack_rejected": "连接被拒绝: {code}"
"engine.connack_rejected_code": "连接被拒绝: {code} (code {n})"
"engine.connect_failed": "连接失败: {e}"
"engine.connect_ok": "连接成功 (session_present: {sp})"
"engine.connecting": "正在连接 {host}:{port} ({protocol}, {transport})"
"engine.connection_error": "连接错误: {e}"
"engine.connection_not_found": "连接不存在"
"engine.disconnect_received": "收到断开连接: {code} (code {n})"
"engine.disconnected_by_server": "服务端断开"
"engine.disconnected_by_server_active": "服务端主动断开"
"engine.disconnected_by_server_code": "服务端断开: {code}"
"engine.events_dropped": "UI 处理不过来,已累计丢弃 {n} 条高频事件(消息/日志)"
"engine.internal_task_failed": "内部任务失败: {e}"
"engine.puback": "发布确认 (packet {n})"
"engine.puback_reason": "发布确认 (packet {n}, reason {reason})"
"engine.pubcomp": "QoS2 发布完成 (packet {n})"
"engine.publish_failed": "发布失败: {e}"
"engine.publish_no_connection": "发布失败: 连接不存在"
"engine.publish_outgoing": "消息已发出 (packet {n})"
"engine.publish_sent": "发布消息到 {topic} (QoS {qos}, retain={retain})"
"engine.pubrec": "QoS2 发布收到 (packet {n})"
"engine.received_message": "收到消息: {topic} ({n} 字节)"
"engine.received_message_qos": "收到消息: {topic} ({n} 字节, QoS {qos})"
"engine.reconnect_give_up": "已重连 {attempts} 次仍失败,达到最大重连次数 {max},停止重连: {e}"
"engine.reconnect_retry": "连接错误,{delay} 秒后重连(第 {attempts} 次): {e}"
"engine.suback": "订阅确认 (packet {n})"
"engine.suback_codes": "订阅确认 (packet {n}): {codes}"
"engine.subscribe_rejected": "订阅被拒绝: {e}"
"engine.subscribe_request_failed": "订阅请求失败: {e}"
"engine.subscribe_topic": "订阅主题: {topic} (QoS {qos})"
"engine.test_timeout": "连接测试超时({secs} 秒),请检查主机地址与端口"
"engine.unsuback": "取消订阅确认"
"engine.unsuback_packet": "取消订阅确认 (packet {n})"
"engine.unsubscribe_failed": "取消订阅失败: {e}"
"engine.unsubscribe_ok": "取消订阅: {topic}"
"engine.user_disconnect": "用户请求断开连接"
"form.auth_section": "认证与心跳" "form.auth_section": "认证与心跳"
"form.auto_connect": "启动时自动连接" "form.auto_connect": "启动时自动连接"
"form.auto_reconnect": "断线自动重连" "form.auto_reconnect": "断线自动重连"
@@ -312,6 +356,19 @@
"titlebar.switch_light": "切换为浅色" "titlebar.switch_light": "切换为浅色"
"titlebar.unpin": "取消窗口置顶" "titlebar.unpin": "取消窗口置顶"
"titlebar.variables": "全局变量" "titlebar.variables": "全局变量"
"tls.ca_invalid_cert": "CA 文件 {p} 中存在无效证书: {e}"
"tls.ca_no_certs": "CA 文件 {p} 中没有找到任何证书"
"tls.ca_parse_failed": "解析 CA 文件 {p} 失败: {e}"
"tls.ca_read_failed": "读取 CA 文件 {p} 失败: {e}"
"tls.cert_key_pair_required": "客户端证书与客户端密钥必须同时配置"
"tls.client_auth_failed": "加载客户端证书/私钥失败: {e}"
"tls.client_cert_no_certs": "客户端证书文件 {p} 中没有找到证书"
"tls.client_cert_parse_failed": "解析客户端证书文件 {p} 失败: {e}"
"tls.client_cert_read_failed": "读取客户端证书文件 {p} 失败: {e}"
"tls.client_key_parse_failed": "解析客户端私钥文件 {p} 失败: {e}"
"tls.client_key_read_failed": "读取客户端私钥文件 {p} 失败: {e}"
"tls.init_failed": "TLS 初始化失败: {e}"
"tls.native_roots_failed": "加载系统根证书失败: {detail}"
"update.apply": "立即更新到 v{v}" "update.apply": "立即更新到 v{v}"
"update.check": "检查更新" "update.check": "检查更新"
"update.check_hint": "检查 GitHub Releases 上的新版本" "update.check_hint": "检查 GitHub Releases 上的新版本"
+13 -12
View File
@@ -19,6 +19,7 @@ use sha1::Sha1;
use sha2::Sha256; use sha2::Sha256;
use crate::model::{ConnectionConfig, ProtocolVersion, TransportKind}; use crate::model::{ConnectionConfig, ProtocolVersion, TransportKind};
use crate::ui::i18n;
type HmacSha1 = Hmac<Sha1>; type HmacSha1 = Hmac<Sha1>;
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
@@ -38,8 +39,8 @@ impl AliyunAuthMode {
pub fn label(self) -> &'static str { pub fn label(self) -> &'static str {
match self { match self {
Self::Token => "Group 令牌 (Token)", Self::Token => i18n::t("aliyun.auth_token_mode"),
Self::DeviceCredential => "一机一密", Self::DeviceCredential => i18n::t("aliyun.auth_triple_mode"),
} }
} }
} }
@@ -83,7 +84,7 @@ impl AliyunPreset {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
name: "阿里云设备".into(), name: i18n::t("aliyun.default_preset_name").into(),
group: String::new(), group: String::new(),
instance_id: String::new(), instance_id: String::new(),
region: String::new(), region: String::new(),
@@ -99,16 +100,16 @@ impl AliyunPreset {
/// HMAC-SHA1(secret, client_id) 的 Base64 编码(Token 模式签名)。 /// HMAC-SHA1(secret, client_id) 的 Base64 编码(Token 模式签名)。
pub fn signature(secret: &str, client_id: &str) -> Result<String, String> { pub fn signature(secret: &str, client_id: &str) -> Result<String, String> {
let mut mac = let mut mac = HmacSha1::new_from_slice(secret.as_bytes())
HmacSha1::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?; .map_err(|e| i18n::tf("aliyun.hmac_init_failed", &[("e", &e)]))?;
mac.update(client_id.as_bytes()); mac.update(client_id.as_bytes());
Ok(base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes())) Ok(base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes()))
} }
/// HMAC-SHA256(secret, msg) 的小写 hex(一机一密签名)。 /// HMAC-SHA256(secret, msg) 的小写 hex(一机一密签名)。
fn hmac_sha256_hex(secret: &str, msg: &str) -> Result<String, String> { fn hmac_sha256_hex(secret: &str, msg: &str) -> Result<String, String> {
let mut mac = let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
HmacSha256::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?; .map_err(|e| i18n::tf("aliyun.hmac_init_failed", &[("e", &e)]))?;
mac.update(msg.as_bytes()); mac.update(msg.as_bytes());
Ok(mac Ok(mac
.finalize() .finalize()
@@ -152,15 +153,15 @@ impl AliyunPreset {
|| self.access_key_id.is_empty() || self.access_key_id.is_empty()
|| self.access_key_secret.is_empty() || self.access_key_secret.is_empty()
{ {
return Err("Group ID、AccessKey ID 与 AccessKey Secret 不能为空".into()); return Err(i18n::t("aliyun.token_required_fields").into());
} }
// 空 instance_id 会拼出 ".mqtt.aliyuncs.com" 这样的非法接入点,DNS 报错难懂 // 空 instance_id 会拼出 ".mqtt.aliyuncs.com" 这样的非法接入点,DNS 报错难懂
if self.instance_id.trim().is_empty() { if self.instance_id.trim().is_empty() {
return Err("Instance ID 不能为空".into()); return Err(i18n::t("aliyun.instance_id_required").into());
} }
// 空 DeviceName 会拼出 "group@@@" 这样的非法 clientId,服务端报错难懂 // 空 DeviceName 会拼出 "group@@@" 这样的非法 clientId,服务端报错难懂
if self.device_id.trim().is_empty() { if self.device_id.trim().is_empty() {
return Err("Device ID 不能为空".into()); return Err(i18n::t("aliyun.device_id_required").into());
} }
conn.host = self.host(); conn.host = self.host();
let client_id = format!("{}@@@{}", self.group_id, self.device_id); let client_id = format!("{}@@@{}", self.group_id, self.device_id);
@@ -172,11 +173,11 @@ impl AliyunPreset {
} }
AliyunAuthMode::DeviceCredential => { AliyunAuthMode::DeviceCredential => {
if self.product_key.is_empty() || self.device_secret.is_empty() { if self.product_key.is_empty() || self.device_secret.is_empty() {
return Err("一机一密需要 ProductKey 与 DeviceSecret (设备密钥)".into()); return Err(i18n::t("aliyun.triple_requires_pk_ds").into());
} }
// 空 DeviceName 会拼出 "pk.|securemode=..." 与 "&pk",服务端报错难懂 // 空 DeviceName 会拼出 "pk.|securemode=..." 与 "&pk",服务端报错难懂
if self.device_id.trim().is_empty() { if self.device_id.trim().is_empty() {
return Err("一机一密需要 DeviceName(设备名)".into()); return Err(i18n::t("aliyun.triple_requires_device_name").into());
} }
conn.host = self.host(); conn.host = self.host();
// 物联网平台「一机一密」直连三元组(见模块文档),DeviceName 复用 device_id // 物联网平台「一机一密」直连三元组(见模块文档),DeviceName 复用 device_id
+49 -29
View File
@@ -20,7 +20,8 @@ use tokio::sync::watch;
use crate::model::{ use crate::model::{
ConnectionConfig, ConnectionStatus, LogEntry, LogLevel, MqttRecord, PublishParams, SubscribeOptions, Subscription, ConnectionConfig, ConnectionStatus, LogEntry, LogLevel, MqttRecord, PublishParams, SubscribeOptions, Subscription,
};// ─── 引擎事件 ──────────────────────────────────────────────────────────────── };
use crate::ui::i18n;// ─── 引擎事件 ────────────────────────────────────────────────────────────────
mod tls; mod tls;
@@ -151,9 +152,7 @@ impl MqttEngine {
connection_id: String::new(), connection_id: String::new(),
level: LogLevel::Warn, level: LogLevel::Warn,
event: "events_dropped".into(), event: "events_dropped".into(),
message: format!( message: i18n::tf("engine.events_dropped", &[("n", &dropped)]),
"UI 处理不过来,已累计丢弃 {dropped} 条高频事件(消息/日志)"
),
details: None, details: None,
})); }));
} }
@@ -221,12 +220,14 @@ impl MqttEngine {
&id, &id,
LogLevel::Info, LogLevel::Info,
"connect", "connect",
format!( i18n::tf(
"正在连接 {}:{} ({}, {})", "engine.connecting",
cfg.host, &[
cfg.port, ("host", &cfg.host),
cfg.protocol.label(), ("port", &cfg.port),
cfg.transport.label() ("protocol", &cfg.protocol.label()),
("transport", &cfg.transport.label()),
],
), ),
None, None,
); );
@@ -259,7 +260,13 @@ impl MqttEngine {
reason_code: None, reason_code: None,
session_present: None, session_present: None,
}); });
engine.log(id, LogLevel::Error, "connect_error", format!("连接失败: {e}"), None); engine.log(
id,
LogLevel::Error,
"connect_error",
i18n::tf("engine.connect_failed", &[("e", &e)]),
None,
);
}; };
let (client, eventloop) = match build { let (client, eventloop) = match build {
Ok(Ok(pair)) => pair, Ok(Ok(pair)) => pair,
@@ -268,7 +275,12 @@ impl MqttEngine {
return; return;
} }
Err(j) => { Err(j) => {
build_error(&engine, &id, generation, format!("内部任务失败: {j}")); build_error(
&engine,
&id,
generation,
i18n::tf("engine.internal_task_failed", &[("e", &j)]),
);
return; return;
} }
}; };
@@ -335,7 +347,13 @@ impl MqttEngine {
connected.store(false, Ordering::SeqCst); connected.store(false, Ordering::SeqCst);
}); });
if user_initiated { if user_initiated {
self.log(id, LogLevel::Info, "disconnect", "用户请求断开连接".into(), None); self.log(
id,
LogLevel::Info,
"disconnect",
i18n::t("engine.user_disconnect").into(),
None,
);
self.emit(EngineEvent::Status { self.emit(EngineEvent::Status {
connection_id: id.to_string(), connection_id: id.to_string(),
status: ConnectionStatus::Disconnected, status: ConnectionStatus::Disconnected,
@@ -357,7 +375,7 @@ impl MqttEngine {
&connection_id, &connection_id,
LogLevel::Error, LogLevel::Error,
"publish_error", "publish_error",
"发布失败: 连接不存在".into(), i18n::t("engine.publish_no_connection").into(),
None, None,
); );
return; return;
@@ -405,9 +423,13 @@ impl MqttEngine {
&connection_id, &connection_id,
LogLevel::Info, LogLevel::Info,
"publish_sent", "publish_sent",
format!( i18n::tf(
"发布消息到 {} (QoS {}, retain={})", "engine.publish_sent",
params.topic, params.qos, params.retain &[
("topic", &params.topic),
("qos", &params.qos),
("retain", &params.retain),
],
), ),
None, None,
); );
@@ -429,7 +451,7 @@ impl MqttEngine {
&connection_id, &connection_id,
LogLevel::Error, LogLevel::Error,
"publish_error", "publish_error",
format!("发布失败: {e}"), i18n::tf("engine.publish_failed", &[("e", &e)]),
None, None,
); );
} }
@@ -460,7 +482,7 @@ impl MqttEngine {
topic: sub.topic, topic: sub.topic,
qos: sub.qos, qos: sub.qos,
ok: false, ok: false,
error: Some("连接不存在".into()), error: Some(i18n::t("engine.connection_not_found").into()),
}); });
return; return;
}; };
@@ -468,7 +490,7 @@ impl MqttEngine {
&connection_id, &connection_id,
LogLevel::Info, LogLevel::Info,
"subscribe", "subscribe",
format!("订阅主题: {} (QoS {})", sub.topic, sub.qos), i18n::tf("engine.subscribe_topic", &[("topic", &sub.topic), ("qos", &sub.qos)]),
None, None,
); );
let result = match &client { let result = match &client {
@@ -493,9 +515,9 @@ impl MqttEngine {
.await .await
{ {
Ok(notice) => notice.wait_completion_async().await.map_err(|e| { Ok(notice) => notice.wait_completion_async().await.map_err(|e| {
format!("订阅被拒绝: {e}") i18n::tf("engine.subscribe_rejected", &[("e", &e)])
}), }),
Err(e) => Err(format!("订阅请求失败: {e}")), Err(e) => Err(i18n::tf("engine.subscribe_request_failed", &[("e", &e)])),
} }
} }
ClientKind::V4(client) => { ClientKind::V4(client) => {
@@ -504,9 +526,9 @@ impl MqttEngine {
.await .await
{ {
Ok(notice) => notice.wait_completion_async().await.map_err(|e| { Ok(notice) => notice.wait_completion_async().await.map_err(|e| {
format!("订阅被拒绝: {e}") i18n::tf("engine.subscribe_rejected", &[("e", &e)])
}), }),
Err(e) => Err(format!("订阅请求失败: {e}")), Err(e) => Err(i18n::tf("engine.subscribe_request_failed", &[("e", &e)])),
} }
} }
}; };
@@ -555,7 +577,7 @@ impl MqttEngine {
&connection_id, &connection_id,
LogLevel::Info, LogLevel::Info,
"unsubscribe", "unsubscribe",
format!("取消订阅: {topic}"), i18n::tf("engine.unsubscribe_ok", &[("topic", &topic)]),
None, None,
); );
} }
@@ -564,7 +586,7 @@ impl MqttEngine {
&connection_id, &connection_id,
LogLevel::Error, LogLevel::Error,
"unsubscribe_error", "unsubscribe_error",
format!("取消订阅失败: {e}"), i18n::tf("engine.unsubscribe_failed", &[("e", &e)]),
None, None,
); );
} }
@@ -618,9 +640,7 @@ impl MqttEngine {
.await; .await;
let reply = match result { let reply = match result {
Ok(inner) => inner, Ok(inner) => inner,
Err(_) => Err(format!( Err(_) => Err(i18n::tf("engine.test_timeout", &[("secs", &secs)])),
"连接测试超时({secs} 秒),请检查主机地址与端口"
)),
}; };
let _ = tx.try_send(reply); let _ = tx.try_send(reply);
}); });
+26 -22
View File
@@ -5,10 +5,8 @@ use std::sync::Arc;
use rumqttc::TlsConfiguration; use rumqttc::TlsConfiguration;
use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer}; use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
use crate::model::{ use crate::model::{ConnectionConfig, SslConfig};
ConnectionConfig, use crate::ui::i18n;// ─── 引擎事件 ────────────────────────────────────────────────────────────────
SslConfig,
};// ─── 引擎事件 ────────────────────────────────────────────────────────────────
@@ -28,7 +26,7 @@ pub(super) fn resolve_tls_config(cfg: &ConnectionConfig) -> Result<Option<TlsCon
pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result<Arc<rustls::ClientConfig>, String> { pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result<Arc<rustls::ClientConfig>, String> {
// 只填证书或只填私钥必然握手失败,提前给出明确错误 // 只填证书或只填私钥必然握手失败,提前给出明确错误
if ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty() { if ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty() {
return Err("客户端证书与客户端密钥必须同时配置".into()); return Err(i18n::t("tls.cert_key_pair_required").into());
} }
let provider = ensure_crypto_provider(); let provider = ensure_crypto_provider();
let builder = rustls::ClientConfig::builder(); let builder = rustls::ClientConfig::builder();
@@ -48,7 +46,7 @@ pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result<Arc<rustls::ClientC
.map(|e| e.to_string()) .map(|e| e.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("; "); .join("; ");
return Err(format!("加载系统根证书失败: {detail}")); return Err(i18n::tf("tls.native_roots_failed", &[("detail", &detail)]));
} }
// 个别系统证书加载失败不阻断连接:自定义 CA 与其余根证书仍参与校验 // 个别系统证书加载失败不阻断连接:自定义 CA 与其余根证书仍参与校验
for cert in native.certs { for cert in native.certs {
@@ -56,17 +54,19 @@ pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result<Arc<rustls::ClientC
} }
if !ssl.ca_file.is_empty() { if !ssl.ca_file.is_empty() {
let pem = std::fs::read(&ssl.ca_file) let pem = std::fs::read(&ssl.ca_file)
.map_err(|e| format!("读取 CA 文件 {} 失败: {e}", ssl.ca_file))?; .map_err(|e| i18n::tf("tls.ca_read_failed", &[("p", &ssl.ca_file), ("e", &e)]))?;
let mut added = 0usize; let mut added = 0usize;
for cert in CertificateDer::pem_slice_iter(&pem) { for cert in CertificateDer::pem_slice_iter(&pem) {
let cert = cert.map_err(|e| format!("解析 CA 文件 {} 失败: {e}", ssl.ca_file))?; let cert = cert.map_err(|e| {
roots i18n::tf("tls.ca_parse_failed", &[("p", &ssl.ca_file), ("e", &e)])
.add(cert) })?;
.map_err(|e| format!("CA 文件 {} 中存在无效证书: {e}", ssl.ca_file))?; roots.add(cert).map_err(|e| {
i18n::tf("tls.ca_invalid_cert", &[("p", &ssl.ca_file), ("e", &e)])
})?;
added += 1; added += 1;
} }
if added == 0 { if added == 0 {
return Err(format!("CA 文件 {} 中没有找到任何证书", ssl.ca_file)); return Err(i18n::tf("tls.ca_no_certs", &[("p", &ssl.ca_file)]));
} }
} }
builder.with_root_certificates(roots) builder.with_root_certificates(roots)
@@ -76,24 +76,28 @@ pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result<Arc<rustls::ClientC
builder.with_no_client_auth() builder.with_no_client_auth()
} else { } else {
let chain_pem = std::fs::read(&ssl.client_cert_file).map_err(|e| { let chain_pem = std::fs::read(&ssl.client_cert_file).map_err(|e| {
format!("读取客户端证书文件 {} 失败: {e}", ssl.client_cert_file) i18n::tf("tls.client_cert_read_failed", &[("p", &ssl.client_cert_file), ("e", &e)])
})?;
let key_pem = std::fs::read(&ssl.client_key_file).map_err(|e| {
i18n::tf("tls.client_key_read_failed", &[("p", &ssl.client_key_file), ("e", &e)])
})?; })?;
let key_pem = std::fs::read(&ssl.client_key_file)
.map_err(|e| format!("读取客户端私钥文件 {} 失败: {e}", ssl.client_key_file))?;
let certs = CertificateDer::pem_slice_iter(&chain_pem) let certs = CertificateDer::pem_slice_iter(&chain_pem)
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("解析客户端证书文件 {} 失败: {e}", ssl.client_cert_file))?; .map_err(|e| {
i18n::tf("tls.client_cert_parse_failed", &[("p", &ssl.client_cert_file), ("e", &e)])
})?;
if certs.is_empty() { if certs.is_empty() {
return Err(format!( return Err(i18n::tf(
"客户端证书文件 {} 中没有找到证书", "tls.client_cert_no_certs",
ssl.client_cert_file &[("p", &ssl.client_cert_file)],
)); ));
} }
let key = PrivateKeyDer::from_pem_slice(&key_pem) let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(|e| {
.map_err(|e| format!("解析客户端私钥文件 {} 失败: {e}", ssl.client_key_file))?; i18n::tf("tls.client_key_parse_failed", &[("p", &ssl.client_key_file), ("e", &e)])
})?;
builder builder
.with_client_auth_cert(certs, key) .with_client_auth_cert(certs, key)
.map_err(|e| format!("加载客户端证书/私钥失败: {e}"))? .map_err(|e| i18n::tf("tls.client_auth_failed", &[("e", &e)]))?
}; };
Ok(Arc::new(config)) Ok(Arc::new(config))
} }
+32 -21
View File
@@ -16,6 +16,7 @@ use crate::model::{
use super::*; use super::*;
use crate::ui::i18n;
pub(super) fn build_v4( pub(super) fn build_v4(
cfg: &ConnectionConfig, cfg: &ConnectionConfig,
@@ -54,7 +55,7 @@ pub(super) fn build_v4(
None => { None => {
opts.set_transport( opts.set_transport(
V4Transport::try_tls_with_default_config() V4Transport::try_tls_with_default_config()
.map_err(|e| format!("TLS 初始化失败: {e:?}"))?, .map_err(|e| i18n::tf("tls.init_failed", &[("e", &format!("{e:?}"))]))?,
); );
} }
}; };
@@ -107,7 +108,7 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
let owned = cfg.clone(); let owned = cfg.clone();
let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v5(&owned)) let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v5(&owned))
.await .await
.map_err(|e| format!("内部任务失败: {e}"))??; .map_err(|e| i18n::tf("engine.internal_task_failed", &[("e", &e)]))??;
loop { loop {
match eventloop.poll().await { match eventloop.poll().await {
Ok(V5Event::Incoming(V5Packet::ConnAck(ack))) => { Ok(V5Event::Incoming(V5Packet::ConnAck(ack))) => {
@@ -123,12 +124,18 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
} }
return Ok(()); return Ok(());
} }
return Err(format!("连接被拒绝: {:?}", ack.code)); return Err(i18n::tf(
"engine.connack_rejected",
&[("code", &format!("{:?}", ack.code))],
));
} }
Ok(V5Event::Incoming(V5Packet::Disconnect(d))) => { Ok(V5Event::Incoming(V5Packet::Disconnect(d))) => {
return Err(format!("服务端断开: {:?}", d.reason_code)); return Err(i18n::tf(
"engine.disconnected_by_server_code",
&[("code", &format!("{:?}", d.reason_code))],
));
} }
Err(e) => return Err(format!("连接失败: {e}")), Err(e) => return Err(i18n::tf("engine.connect_failed", &[("e", &e)])),
_ => {} _ => {}
} }
} }
@@ -137,7 +144,7 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
let owned = cfg.clone(); let owned = cfg.clone();
let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v4(&owned)) let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v4(&owned))
.await .await
.map_err(|e| format!("内部任务失败: {e}"))??; .map_err(|e| i18n::tf("engine.internal_task_failed", &[("e", &e)]))??;
loop { loop {
match eventloop.poll().await { match eventloop.poll().await {
Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => { Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => {
@@ -151,12 +158,15 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
} }
return Ok(()); return Ok(());
} }
return Err(format!("连接被拒绝: {:?}", ack.code)); return Err(i18n::tf(
"engine.connack_rejected",
&[("code", &format!("{:?}", ack.code))],
));
} }
Ok(rv4::Event::Incoming(rv4::Packet::Disconnect)) => { Ok(rv4::Event::Incoming(rv4::Packet::Disconnect)) => {
return Err("服务端主动断开".into()); return Err(i18n::t("engine.disconnected_by_server_active").into());
} }
Err(e) => return Err(format!("连接失败: {e}")), Err(e) => return Err(i18n::tf("engine.connect_failed", &[("e", &e)])),
_ => {} _ => {}
} }
} }
@@ -213,7 +223,7 @@ pub(super) fn spawn_v4_loop(
}; };
engine.log( engine.log(
&id, LogLevel::Info, "publish_received", &id, LogLevel::Info, "publish_received",
format!("收到消息: {} ({} 字节)", record.topic, p.payload.len()), None); i18n::tf("engine.received_message", &[("topic", &record.topic), ("n", &p.payload.len())]), None);
engine.emit(EngineEvent::Message(record)); engine.emit(EngineEvent::Message(record));
} }
Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => { Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => {
@@ -224,7 +234,7 @@ pub(super) fn spawn_v4_loop(
// 「连续失败次数」,不能把历史错误累计进来 // 「连续失败次数」,不能把历史错误累计进来
reconnect_attempts = 0; reconnect_attempts = 0;
engine.log(&id, LogLevel::Info, "connack", engine.log(&id, LogLevel::Info, "connack",
format!("连接成功 (session_present: {})", ack.session_present), None); i18n::tf("engine.connect_ok", &[("sp", &ack.session_present)]), None);
engine.emit(EngineEvent::Status { engine.emit(EngineEvent::Status {
connection_id: id.clone(), connection_id: id.clone(),
status: ConnectionStatus::Connected, status: ConnectionStatus::Connected,
@@ -234,7 +244,7 @@ pub(super) fn spawn_v4_loop(
}); });
} else { } else {
engine.log(&id, LogLevel::Error, "connack", engine.log(&id, LogLevel::Error, "connack",
format!("连接被拒绝: {:?}", ack.code), None); i18n::tf("engine.connack_rejected", &[("code", &format!("{:?}", ack.code))]), None);
engine.emit(EngineEvent::Status { engine.emit(EngineEvent::Status {
connection_id: id.clone(), connection_id: id.clone(),
status: ConnectionStatus::Error, status: ConnectionStatus::Error,
@@ -251,7 +261,7 @@ pub(super) fn spawn_v4_loop(
engine.emit(EngineEvent::Status { engine.emit(EngineEvent::Status {
connection_id: id.clone(), connection_id: id.clone(),
status: ConnectionStatus::Disconnected, status: ConnectionStatus::Disconnected,
error: Some("服务端断开".into()), error: Some(i18n::t("engine.disconnected_by_server").into()),
reason_code: None, reason_code: None,
session_present: None, session_present: None,
}); });
@@ -259,19 +269,19 @@ pub(super) fn spawn_v4_loop(
} }
Ok(rv4::Event::Incoming(rv4::Packet::SubAck(s))) => { Ok(rv4::Event::Incoming(rv4::Packet::SubAck(s))) => {
engine.log(&id, LogLevel::Info, "suback", engine.log(&id, LogLevel::Info, "suback",
format!("订阅确认 (packet {})", s.pkid), None); i18n::tf("engine.suback", &[("n", &s.pkid)]), None);
} }
Ok(rv4::Event::Incoming(rv4::Packet::UnsubAck(a))) => { Ok(rv4::Event::Incoming(rv4::Packet::UnsubAck(a))) => {
engine.log(&id, LogLevel::Info, "unsuback", engine.log(&id, LogLevel::Info, "unsuback",
format!("取消订阅确认 (packet {})", a.pkid), None); i18n::tf("engine.unsuback_packet", &[("n", &a.pkid)]), None);
} }
Ok(rv4::Event::Incoming(rv4::Packet::PubAck(a))) => { Ok(rv4::Event::Incoming(rv4::Packet::PubAck(a))) => {
engine.log(&id, LogLevel::Debug, "puback", engine.log(&id, LogLevel::Debug, "puback",
format!("发布确认 (packet {})", a.pkid), None); i18n::tf("engine.puback", &[("n", &a.pkid)]), None);
} }
Ok(rv4::Event::Outgoing(rv4::Outgoing::Publish(pkid))) => { Ok(rv4::Event::Outgoing(rv4::Outgoing::Publish(pkid))) => {
engine.log(&id, LogLevel::Debug, "publish_outgoing", engine.log(&id, LogLevel::Debug, "publish_outgoing",
format!("消息已发出 (packet {pkid})"), None); i18n::tf("engine.publish_outgoing", &[("n", &pkid)]), None);
} }
Ok(_) => {} Ok(_) => {}
Err(rv4::ConnectionError::RequestsDone) => { Err(rv4::ConnectionError::RequestsDone) => {
@@ -293,19 +303,20 @@ pub(super) fn spawn_v4_loop(
}); });
if !auto_reconnect || first_connect { if !auto_reconnect || first_connect {
engine.log(&id, LogLevel::Error, "connection_error", engine.log(&id, LogLevel::Error, "connection_error",
format!("连接错误: {e}"), None); i18n::tf("engine.connection_error", &[("e", &e)]), None);
break; break;
} }
// max_reconnect_times>0 时按次数停止重试;0 表示无限重连 // max_reconnect_times>0 时按次数停止重试;0 表示无限重连
if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times { if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times {
engine.log(&id, LogLevel::Error, "reconnect_give_up", engine.log(&id, LogLevel::Error, "reconnect_give_up",
format!("已重连 {reconnect_attempts} 次仍失败,达到最大重连次数 {max_reconnect_times},停止重连: {e}"), None); i18n::tf("engine.reconnect_give_up", &[("attempts", &reconnect_attempts), ("max", &max_reconnect_times), ("e", &e)]), None);
break; break;
} }
reconnect_attempts += 1; reconnect_attempts += 1;
let delay = reconnect_delay(reconnect_attempts); let delay = reconnect_delay(reconnect_attempts);
let delay_s = format!("{:.1}", delay.as_secs_f32());
engine.log(&id, LogLevel::Warn, "connection_error", engine.log(&id, LogLevel::Warn, "connection_error",
format!("连接错误,{:.1} 秒后重连(第 {reconnect_attempts} 次): {e}", delay.as_secs_f32()), None); i18n::tf("engine.reconnect_retry", &[("delay", &delay_s), ("attempts", &reconnect_attempts), ("e", &e)]), None);
// 退避等待也必须可取消:否则 close() 的取消信号要等一整个 // 退避等待也必须可取消:否则 close() 的取消信号要等一整个
// 退避周期才可见,期间会对用户已关闭的连接继续重连。 // 退避周期才可见,期间会对用户已关闭的连接继续重连。
tokio::select! { tokio::select! {
@@ -316,7 +327,7 @@ pub(super) fn spawn_v4_loop(
} }
} }
_ = cancel_rx.changed() => { _ = cancel_rx.changed() => {
engine.log(&id, LogLevel::Info, "disconnect_cancelled", "连接已取消".into(), None); engine.log(&id, LogLevel::Info, "disconnect_cancelled", i18n::t("engine.cancelled").into(), None);
break; break;
} }
} }
+33 -15
View File
@@ -19,6 +19,7 @@ use crate::model::{
use super::*; use super::*;
use crate::ui::i18n;
pub(super) fn build_v5( pub(super) fn build_v5(
cfg: &ConnectionConfig, cfg: &ConnectionConfig,
@@ -55,7 +56,7 @@ pub(super) fn build_v5(
None => { None => {
opts.set_transport( opts.set_transport(
V5Transport::try_tls_with_default_config() V5Transport::try_tls_with_default_config()
.map_err(|e| format!("TLS 初始化失败: {e:?}"))?, .map_err(|e| i18n::tf("tls.init_failed", &[("e", &format!("{e:?}"))]))?,
); );
} }
}; };
@@ -208,7 +209,14 @@ pub(super) fn spawn_v5_loop(
&id, &id,
LogLevel::Info, LogLevel::Info,
"publish_received", "publish_received",
format!("收到消息: {topic} ({} 字节, QoS {})", p.payload.len(), v5_qos_u8(p.qos)), i18n::tf(
"engine.received_message_qos",
&[
("topic", &topic),
("n", &p.payload.len()),
("qos", &v5_qos_u8(p.qos)),
],
),
None, None,
); );
engine.emit(EngineEvent::Message(record)); engine.emit(EngineEvent::Message(record));
@@ -225,7 +233,7 @@ pub(super) fn spawn_v5_loop(
&id, &id,
LogLevel::Info, LogLevel::Info,
"connack", "connack",
format!("连接成功 (session_present: {})", ack.session_present), i18n::tf("engine.connect_ok", &[("sp", &ack.session_present)]),
None, None,
); );
engine.emit(EngineEvent::Status { engine.emit(EngineEvent::Status {
@@ -240,7 +248,10 @@ pub(super) fn spawn_v5_loop(
&id, &id,
LogLevel::Error, LogLevel::Error,
"connack", "connack",
format!("连接被拒绝: {:?} (code {code})", ack.code), i18n::tf(
"engine.connack_rejected_code",
&[("code", &format!("{:?}", ack.code)), ("n", &code)],
),
None, None,
); );
engine.emit(EngineEvent::Status { engine.emit(EngineEvent::Status {
@@ -259,7 +270,13 @@ pub(super) fn spawn_v5_loop(
&id, &id,
LogLevel::Warn, LogLevel::Warn,
"disconnect_received", "disconnect_received",
format!("收到断开连接: {:?} (code {})", d.reason_code, d.reason_code as u8), i18n::tf(
"engine.disconnect_received",
&[
("code", &format!("{:?}", d.reason_code)),
("n", &(d.reason_code as u8)),
],
),
None, None,
); );
engine.emit(EngineEvent::Status { engine.emit(EngineEvent::Status {
@@ -278,26 +295,26 @@ pub(super) fn spawn_v5_loop(
.map(|c| format!("{c:?}")) .map(|c| format!("{c:?}"))
.collect(); .collect();
engine.log(&id, LogLevel::Info, "suback", engine.log(&id, LogLevel::Info, "suback",
format!("订阅确认 (packet {}): {}", s.pkid, granted.join(", ")), None); i18n::tf("engine.suback_codes", &[("n", &s.pkid), ("codes", &granted.join(", "))]), None);
} }
Ok(V5Event::Incoming(V5Packet::UnsubAck(_))) => { Ok(V5Event::Incoming(V5Packet::UnsubAck(_))) => {
engine.log(&id, LogLevel::Info, "unsuback", "取消订阅确认".into(), None); engine.log(&id, LogLevel::Info, "unsuback", i18n::t("engine.unsuback").into(), None);
} }
Ok(V5Event::Incoming(V5Packet::PubAck(a))) => { Ok(V5Event::Incoming(V5Packet::PubAck(a))) => {
engine.log(&id, LogLevel::Debug, "puback", engine.log(&id, LogLevel::Debug, "puback",
format!("发布确认 (packet {}, reason {:?})", a.pkid, a.reason), None); i18n::tf("engine.puback_reason", &[("n", &a.pkid), ("reason", &format!("{:?}", a.reason))]), None);
} }
Ok(V5Event::Incoming(V5Packet::PubRec(a))) => { Ok(V5Event::Incoming(V5Packet::PubRec(a))) => {
engine.log(&id, LogLevel::Debug, "pubrec", engine.log(&id, LogLevel::Debug, "pubrec",
format!("QoS2 发布收到 (packet {})", a.pkid), None); i18n::tf("engine.pubrec", &[("n", &a.pkid)]), None);
} }
Ok(V5Event::Incoming(V5Packet::PubComp(a))) => { Ok(V5Event::Incoming(V5Packet::PubComp(a))) => {
engine.log(&id, LogLevel::Debug, "pubcomp", engine.log(&id, LogLevel::Debug, "pubcomp",
format!("QoS2 发布完成 (packet {})", a.pkid), None); i18n::tf("engine.pubcomp", &[("n", &a.pkid)]), None);
} }
Ok(V5Event::Outgoing(V5Outgoing::Publish(pkid))) => { Ok(V5Event::Outgoing(V5Outgoing::Publish(pkid))) => {
engine.log(&id, LogLevel::Debug, "publish_outgoing", engine.log(&id, LogLevel::Debug, "publish_outgoing",
format!("消息已发出 (packet {pkid})"), None); i18n::tf("engine.publish_outgoing", &[("n", &pkid)]), None);
} }
Ok(_) => {} Ok(_) => {}
Err(V5Error::RequestsDone) => { Err(V5Error::RequestsDone) => {
@@ -322,19 +339,20 @@ pub(super) fn spawn_v5_loop(
}); });
if !auto_reconnect || first_connect { if !auto_reconnect || first_connect {
engine.log(&id, LogLevel::Error, "connection_error", engine.log(&id, LogLevel::Error, "connection_error",
format!("连接错误: {e}"), None); i18n::tf("engine.connection_error", &[("e", &e)]), None);
break; break;
} }
// max_reconnect_times>0 时按次数停止重试;0 表示无限重连 // max_reconnect_times>0 时按次数停止重试;0 表示无限重连
if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times { if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times {
engine.log(&id, LogLevel::Error, "reconnect_give_up", engine.log(&id, LogLevel::Error, "reconnect_give_up",
format!("已重连 {reconnect_attempts} 次仍失败,达到最大重连次数 {max_reconnect_times},停止重连: {e}"), None); i18n::tf("engine.reconnect_give_up", &[("attempts", &reconnect_attempts), ("max", &max_reconnect_times), ("e", &e)]), None);
break; break;
} }
reconnect_attempts += 1; reconnect_attempts += 1;
let delay = reconnect_delay(reconnect_attempts); let delay = reconnect_delay(reconnect_attempts);
let delay_s = format!("{:.1}", delay.as_secs_f32());
engine.log(&id, LogLevel::Warn, "connection_error", engine.log(&id, LogLevel::Warn, "connection_error",
format!("连接错误,{:.1} 秒后重连(第 {reconnect_attempts} 次): {e}", delay.as_secs_f32()), None); i18n::tf("engine.reconnect_retry", &[("delay", &delay_s), ("attempts", &reconnect_attempts), ("e", &e)]), None);
// 退避等待也必须可取消:否则 close() 的取消信号要等一整个 // 退避等待也必须可取消:否则 close() 的取消信号要等一整个
// 退避周期才可见,期间会对用户已关闭的连接继续重连。 // 退避周期才可见,期间会对用户已关闭的连接继续重连。
tokio::select! { tokio::select! {
@@ -345,7 +363,7 @@ pub(super) fn spawn_v5_loop(
} }
} }
_ = cancel_rx.changed() => { _ = cancel_rx.changed() => {
engine.log(&id, LogLevel::Info, "disconnect_cancelled", "连接已取消".into(), None); engine.log(&id, LogLevel::Info, "disconnect_cancelled", i18n::t("engine.cancelled").into(), None);
break; break;
} }
} }