fix(i18n): MQTT 引擎与阿里云模块用户可见文案接入语言包
This commit is contained in:
@@ -4,14 +4,18 @@
|
||||
"aliyun.auth_token_mode": "Group token"
|
||||
"aliyun.auth_triple_mode": "Per-device secret"
|
||||
"aliyun.build_failed": "Failed to build connection: {e}"
|
||||
"aliyun.default_preset_name": "Alibaba Cloud device"
|
||||
"aliyun.device_id": "Device ID"
|
||||
"aliyun.device_id_required": "Device ID cannot be empty"
|
||||
"aliyun.device_secret": "DeviceSecret (per-device)"
|
||||
"aliyun.device_secret_ph": "Triple: DeviceSecret"
|
||||
"aliyun.generate_connect": "Generate & connect"
|
||||
"aliyun.group": "Group (optional)"
|
||||
"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.hmac_init_failed": "HMAC init failed: {e}"
|
||||
"aliyun.instance_id": "Instance ID"
|
||||
"aliyun.instance_id_required": "Instance ID cannot be empty"
|
||||
"aliyun.preset_name": "Preset name"
|
||||
"aliyun.product_key": "ProductKey (per-device)"
|
||||
"aliyun.product_key_ph": "Triple: ProductKey"
|
||||
@@ -20,6 +24,9 @@
|
||||
"aliyun.save_preset": "Save preset"
|
||||
"aliyun.saved_presets": "Saved presets"
|
||||
"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.cyan": "Cyan"
|
||||
"color.green": "Green"
|
||||
@@ -57,6 +64,43 @@
|
||||
"encode.hex_failed": "Hex decode failed: {e}"
|
||||
"encode.hex_invalid": "Hex decode failed: non-hex character present"
|
||||
"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.auto_connect": "Connect on app start"
|
||||
"form.auto_reconnect": "Auto reconnect on disconnect"
|
||||
@@ -312,6 +356,19 @@
|
||||
"titlebar.switch_light": "Switch to light theme"
|
||||
"titlebar.unpin": "Unpin window"
|
||||
"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.check": "Check for updates"
|
||||
"update.check_hint": "Check GitHub Releases for a new version"
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
"aliyun.auth_token_mode": "Group 令牌 (Token)"
|
||||
"aliyun.auth_triple_mode": "一机一密"
|
||||
"aliyun.build_failed": "无法生成连接: {e}"
|
||||
"aliyun.default_preset_name": "阿里云设备"
|
||||
"aliyun.device_id": "设备 ID"
|
||||
"aliyun.device_id_required": "Device ID 不能为空"
|
||||
"aliyun.device_secret": "DeviceSecret(一机一密)"
|
||||
"aliyun.device_secret_ph": "一机一密:DeviceSecret"
|
||||
"aliyun.generate_connect": "生成并连接"
|
||||
"aliyun.group": "分组(可选)"
|
||||
"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.hmac_init_failed": "HMAC 初始化失败: {e}"
|
||||
"aliyun.instance_id": "实例 ID"
|
||||
"aliyun.instance_id_required": "Instance ID 不能为空"
|
||||
"aliyun.preset_name": "预设名称"
|
||||
"aliyun.product_key": "ProductKey(一机一密)"
|
||||
"aliyun.product_key_ph": "一机一密:ProductKey"
|
||||
@@ -20,6 +24,9 @@
|
||||
"aliyun.save_preset": "保存预设"
|
||||
"aliyun.saved_presets": "已保存预设"
|
||||
"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.cyan": "青"
|
||||
"color.green": "绿"
|
||||
@@ -57,6 +64,43 @@
|
||||
"encode.hex_failed": "Hex 解码失败: {e}"
|
||||
"encode.hex_invalid": "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.auto_connect": "启动时自动连接"
|
||||
"form.auto_reconnect": "断线自动重连"
|
||||
@@ -312,6 +356,19 @@
|
||||
"titlebar.switch_light": "切换为浅色"
|
||||
"titlebar.unpin": "取消窗口置顶"
|
||||
"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.check": "检查更新"
|
||||
"update.check_hint": "检查 GitHub Releases 上的新版本"
|
||||
|
||||
+13
-12
@@ -19,6 +19,7 @@ use sha1::Sha1;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::model::{ConnectionConfig, ProtocolVersion, TransportKind};
|
||||
use crate::ui::i18n;
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
@@ -38,8 +39,8 @@ impl AliyunAuthMode {
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Token => "Group 令牌 (Token)",
|
||||
Self::DeviceCredential => "一机一密",
|
||||
Self::Token => i18n::t("aliyun.auth_token_mode"),
|
||||
Self::DeviceCredential => i18n::t("aliyun.auth_triple_mode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,7 @@ impl AliyunPreset {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: "阿里云设备".into(),
|
||||
name: i18n::t("aliyun.default_preset_name").into(),
|
||||
group: String::new(),
|
||||
instance_id: String::new(),
|
||||
region: String::new(),
|
||||
@@ -99,16 +100,16 @@ impl AliyunPreset {
|
||||
|
||||
/// HMAC-SHA1(secret, client_id) 的 Base64 编码(Token 模式签名)。
|
||||
pub fn signature(secret: &str, client_id: &str) -> Result<String, String> {
|
||||
let mut mac =
|
||||
HmacSha1::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?;
|
||||
let mut mac = HmacSha1::new_from_slice(secret.as_bytes())
|
||||
.map_err(|e| i18n::tf("aliyun.hmac_init_failed", &[("e", &e)]))?;
|
||||
mac.update(client_id.as_bytes());
|
||||
Ok(base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes()))
|
||||
}
|
||||
|
||||
/// HMAC-SHA256(secret, msg) 的小写 hex(一机一密签名)。
|
||||
fn hmac_sha256_hex(secret: &str, msg: &str) -> Result<String, String> {
|
||||
let mut mac =
|
||||
HmacSha256::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?;
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
||||
.map_err(|e| i18n::tf("aliyun.hmac_init_failed", &[("e", &e)]))?;
|
||||
mac.update(msg.as_bytes());
|
||||
Ok(mac
|
||||
.finalize()
|
||||
@@ -152,15 +153,15 @@ impl AliyunPreset {
|
||||
|| self.access_key_id.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 报错难懂
|
||||
if self.instance_id.trim().is_empty() {
|
||||
return Err("Instance ID 不能为空".into());
|
||||
return Err(i18n::t("aliyun.instance_id_required").into());
|
||||
}
|
||||
// 空 DeviceName 会拼出 "group@@@" 这样的非法 clientId,服务端报错难懂
|
||||
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();
|
||||
let client_id = format!("{}@@@{}", self.group_id, self.device_id);
|
||||
@@ -172,11 +173,11 @@ impl AliyunPreset {
|
||||
}
|
||||
AliyunAuthMode::DeviceCredential => {
|
||||
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",服务端报错难懂
|
||||
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();
|
||||
// 物联网平台「一机一密」直连三元组(见模块文档),DeviceName 复用 device_id
|
||||
|
||||
+49
-29
@@ -20,7 +20,8 @@ use tokio::sync::watch;
|
||||
|
||||
use crate::model::{
|
||||
ConnectionConfig, ConnectionStatus, LogEntry, LogLevel, MqttRecord, PublishParams, SubscribeOptions, Subscription,
|
||||
};// ─── 引擎事件 ────────────────────────────────────────────────────────────────
|
||||
};
|
||||
use crate::ui::i18n;// ─── 引擎事件 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
mod tls;
|
||||
@@ -151,9 +152,7 @@ impl MqttEngine {
|
||||
connection_id: String::new(),
|
||||
level: LogLevel::Warn,
|
||||
event: "events_dropped".into(),
|
||||
message: format!(
|
||||
"UI 处理不过来,已累计丢弃 {dropped} 条高频事件(消息/日志)"
|
||||
),
|
||||
message: i18n::tf("engine.events_dropped", &[("n", &dropped)]),
|
||||
details: None,
|
||||
}));
|
||||
}
|
||||
@@ -221,12 +220,14 @@ impl MqttEngine {
|
||||
&id,
|
||||
LogLevel::Info,
|
||||
"connect",
|
||||
format!(
|
||||
"正在连接 {}:{} ({}, {})",
|
||||
cfg.host,
|
||||
cfg.port,
|
||||
cfg.protocol.label(),
|
||||
cfg.transport.label()
|
||||
i18n::tf(
|
||||
"engine.connecting",
|
||||
&[
|
||||
("host", &cfg.host),
|
||||
("port", &cfg.port),
|
||||
("protocol", &cfg.protocol.label()),
|
||||
("transport", &cfg.transport.label()),
|
||||
],
|
||||
),
|
||||
None,
|
||||
);
|
||||
@@ -259,7 +260,13 @@ impl MqttEngine {
|
||||
reason_code: 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 {
|
||||
Ok(Ok(pair)) => pair,
|
||||
@@ -268,7 +275,12 @@ impl MqttEngine {
|
||||
return;
|
||||
}
|
||||
Err(j) => {
|
||||
build_error(&engine, &id, generation, format!("内部任务失败: {j}"));
|
||||
build_error(
|
||||
&engine,
|
||||
&id,
|
||||
generation,
|
||||
i18n::tf("engine.internal_task_failed", &[("e", &j)]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -335,7 +347,13 @@ impl MqttEngine {
|
||||
connected.store(false, Ordering::SeqCst);
|
||||
});
|
||||
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 {
|
||||
connection_id: id.to_string(),
|
||||
status: ConnectionStatus::Disconnected,
|
||||
@@ -357,7 +375,7 @@ impl MqttEngine {
|
||||
&connection_id,
|
||||
LogLevel::Error,
|
||||
"publish_error",
|
||||
"发布失败: 连接不存在".into(),
|
||||
i18n::t("engine.publish_no_connection").into(),
|
||||
None,
|
||||
);
|
||||
return;
|
||||
@@ -405,9 +423,13 @@ impl MqttEngine {
|
||||
&connection_id,
|
||||
LogLevel::Info,
|
||||
"publish_sent",
|
||||
format!(
|
||||
"发布消息到 {} (QoS {}, retain={})",
|
||||
params.topic, params.qos, params.retain
|
||||
i18n::tf(
|
||||
"engine.publish_sent",
|
||||
&[
|
||||
("topic", ¶ms.topic),
|
||||
("qos", ¶ms.qos),
|
||||
("retain", ¶ms.retain),
|
||||
],
|
||||
),
|
||||
None,
|
||||
);
|
||||
@@ -429,7 +451,7 @@ impl MqttEngine {
|
||||
&connection_id,
|
||||
LogLevel::Error,
|
||||
"publish_error",
|
||||
format!("发布失败: {e}"),
|
||||
i18n::tf("engine.publish_failed", &[("e", &e)]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -460,7 +482,7 @@ impl MqttEngine {
|
||||
topic: sub.topic,
|
||||
qos: sub.qos,
|
||||
ok: false,
|
||||
error: Some("连接不存在".into()),
|
||||
error: Some(i18n::t("engine.connection_not_found").into()),
|
||||
});
|
||||
return;
|
||||
};
|
||||
@@ -468,7 +490,7 @@ impl MqttEngine {
|
||||
&connection_id,
|
||||
LogLevel::Info,
|
||||
"subscribe",
|
||||
format!("订阅主题: {} (QoS {})", sub.topic, sub.qos),
|
||||
i18n::tf("engine.subscribe_topic", &[("topic", &sub.topic), ("qos", &sub.qos)]),
|
||||
None,
|
||||
);
|
||||
let result = match &client {
|
||||
@@ -493,9 +515,9 @@ impl MqttEngine {
|
||||
.await
|
||||
{
|
||||
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) => {
|
||||
@@ -504,9 +526,9 @@ impl MqttEngine {
|
||||
.await
|
||||
{
|
||||
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,
|
||||
LogLevel::Info,
|
||||
"unsubscribe",
|
||||
format!("取消订阅: {topic}"),
|
||||
i18n::tf("engine.unsubscribe_ok", &[("topic", &topic)]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -564,7 +586,7 @@ impl MqttEngine {
|
||||
&connection_id,
|
||||
LogLevel::Error,
|
||||
"unsubscribe_error",
|
||||
format!("取消订阅失败: {e}"),
|
||||
i18n::tf("engine.unsubscribe_failed", &[("e", &e)]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -618,9 +640,7 @@ impl MqttEngine {
|
||||
.await;
|
||||
let reply = match result {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => Err(format!(
|
||||
"连接测试超时({secs} 秒),请检查主机地址与端口"
|
||||
)),
|
||||
Err(_) => Err(i18n::tf("engine.test_timeout", &[("secs", &secs)])),
|
||||
};
|
||||
let _ = tx.try_send(reply);
|
||||
});
|
||||
|
||||
+26
-22
@@ -5,10 +5,8 @@ use std::sync::Arc;
|
||||
use rumqttc::TlsConfiguration;
|
||||
use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
|
||||
|
||||
use crate::model::{
|
||||
ConnectionConfig,
|
||||
SslConfig,
|
||||
};// ─── 引擎事件 ────────────────────────────────────────────────────────────────
|
||||
use crate::model::{ConnectionConfig, SslConfig};
|
||||
use crate::ui::i18n;// ─── 引擎事件 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -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> {
|
||||
// 只填证书或只填私钥必然握手失败,提前给出明确错误
|
||||
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 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())
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
return Err(format!("加载系统根证书失败: {detail}"));
|
||||
return Err(i18n::tf("tls.native_roots_failed", &[("detail", &detail)]));
|
||||
}
|
||||
// 个别系统证书加载失败不阻断连接:自定义 CA 与其余根证书仍参与校验
|
||||
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() {
|
||||
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;
|
||||
for cert in CertificateDer::pem_slice_iter(&pem) {
|
||||
let cert = cert.map_err(|e| format!("解析 CA 文件 {} 失败: {e}", ssl.ca_file))?;
|
||||
roots
|
||||
.add(cert)
|
||||
.map_err(|e| format!("CA 文件 {} 中存在无效证书: {e}", ssl.ca_file))?;
|
||||
let cert = cert.map_err(|e| {
|
||||
i18n::tf("tls.ca_parse_failed", &[("p", &ssl.ca_file), ("e", &e)])
|
||||
})?;
|
||||
roots.add(cert).map_err(|e| {
|
||||
i18n::tf("tls.ca_invalid_cert", &[("p", &ssl.ca_file), ("e", &e)])
|
||||
})?;
|
||||
added += 1;
|
||||
}
|
||||
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)
|
||||
@@ -76,24 +76,28 @@ pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result<Arc<rustls::ClientC
|
||||
builder.with_no_client_auth()
|
||||
} else {
|
||||
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)
|
||||
.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() {
|
||||
return Err(format!(
|
||||
"客户端证书文件 {} 中没有找到证书",
|
||||
ssl.client_cert_file
|
||||
return Err(i18n::tf(
|
||||
"tls.client_cert_no_certs",
|
||||
&[("p", &ssl.client_cert_file)],
|
||||
));
|
||||
}
|
||||
let key = PrivateKeyDer::from_pem_slice(&key_pem)
|
||||
.map_err(|e| format!("解析客户端私钥文件 {} 失败: {e}", ssl.client_key_file))?;
|
||||
let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(|e| {
|
||||
i18n::tf("tls.client_key_parse_failed", &[("p", &ssl.client_key_file), ("e", &e)])
|
||||
})?;
|
||||
builder
|
||||
.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))
|
||||
}
|
||||
|
||||
+32
-21
@@ -16,6 +16,7 @@ use crate::model::{
|
||||
|
||||
|
||||
use super::*;
|
||||
use crate::ui::i18n;
|
||||
|
||||
pub(super) fn build_v4(
|
||||
cfg: &ConnectionConfig,
|
||||
@@ -54,7 +55,7 @@ pub(super) fn build_v4(
|
||||
None => {
|
||||
opts.set_transport(
|
||||
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 (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v5(&owned))
|
||||
.await
|
||||
.map_err(|e| format!("内部任务失败: {e}"))??;
|
||||
.map_err(|e| i18n::tf("engine.internal_task_failed", &[("e", &e)]))??;
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(V5Event::Incoming(V5Packet::ConnAck(ack))) => {
|
||||
@@ -123,12 +124,18 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!("连接被拒绝: {:?}", ack.code));
|
||||
return Err(i18n::tf(
|
||||
"engine.connack_rejected",
|
||||
&[("code", &format!("{:?}", ack.code))],
|
||||
));
|
||||
}
|
||||
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 (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v4(&owned))
|
||||
.await
|
||||
.map_err(|e| format!("内部任务失败: {e}"))??;
|
||||
.map_err(|e| i18n::tf("engine.internal_task_failed", &[("e", &e)]))??;
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
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 Err(format!("连接被拒绝: {:?}", ack.code));
|
||||
return Err(i18n::tf(
|
||||
"engine.connack_rejected",
|
||||
&[("code", &format!("{:?}", ack.code))],
|
||||
));
|
||||
}
|
||||
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(
|
||||
&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));
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => {
|
||||
@@ -224,7 +234,7 @@ pub(super) fn spawn_v4_loop(
|
||||
// 「连续失败次数」,不能把历史错误累计进来
|
||||
reconnect_attempts = 0;
|
||||
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 {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Connected,
|
||||
@@ -234,7 +244,7 @@ pub(super) fn spawn_v4_loop(
|
||||
});
|
||||
} else {
|
||||
engine.log(&id, LogLevel::Error, "connack",
|
||||
format!("连接被拒绝: {:?}", ack.code), None);
|
||||
i18n::tf("engine.connack_rejected", &[("code", &format!("{:?}", ack.code))]), None);
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Error,
|
||||
@@ -251,7 +261,7 @@ pub(super) fn spawn_v4_loop(
|
||||
engine.emit(EngineEvent::Status {
|
||||
connection_id: id.clone(),
|
||||
status: ConnectionStatus::Disconnected,
|
||||
error: Some("服务端断开".into()),
|
||||
error: Some(i18n::t("engine.disconnected_by_server").into()),
|
||||
reason_code: None,
|
||||
session_present: None,
|
||||
});
|
||||
@@ -259,19 +269,19 @@ pub(super) fn spawn_v4_loop(
|
||||
}
|
||||
Ok(rv4::Event::Incoming(rv4::Packet::SubAck(s))) => {
|
||||
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))) => {
|
||||
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))) => {
|
||||
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))) => {
|
||||
engine.log(&id, LogLevel::Debug, "publish_outgoing",
|
||||
format!("消息已发出 (packet {pkid})"), None);
|
||||
i18n::tf("engine.publish_outgoing", &[("n", &pkid)]), None);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(rv4::ConnectionError::RequestsDone) => {
|
||||
@@ -293,19 +303,20 @@ pub(super) fn spawn_v4_loop(
|
||||
});
|
||||
if !auto_reconnect || first_connect {
|
||||
engine.log(&id, LogLevel::Error, "connection_error",
|
||||
format!("连接错误: {e}"), None);
|
||||
i18n::tf("engine.connection_error", &[("e", &e)]), None);
|
||||
break;
|
||||
}
|
||||
// max_reconnect_times>0 时按次数停止重试;0 表示无限重连
|
||||
if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times {
|
||||
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;
|
||||
}
|
||||
reconnect_attempts += 1;
|
||||
let delay = reconnect_delay(reconnect_attempts);
|
||||
let delay_s = format!("{:.1}", delay.as_secs_f32());
|
||||
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() 的取消信号要等一整个
|
||||
// 退避周期才可见,期间会对用户已关闭的连接继续重连。
|
||||
tokio::select! {
|
||||
@@ -316,7 +327,7 @@ pub(super) fn spawn_v4_loop(
|
||||
}
|
||||
}
|
||||
_ = 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;
|
||||
}
|
||||
}
|
||||
|
||||
+33
-15
@@ -19,6 +19,7 @@ use crate::model::{
|
||||
|
||||
|
||||
use super::*;
|
||||
use crate::ui::i18n;
|
||||
|
||||
pub(super) fn build_v5(
|
||||
cfg: &ConnectionConfig,
|
||||
@@ -55,7 +56,7 @@ pub(super) fn build_v5(
|
||||
None => {
|
||||
opts.set_transport(
|
||||
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,
|
||||
LogLevel::Info,
|
||||
"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,
|
||||
);
|
||||
engine.emit(EngineEvent::Message(record));
|
||||
@@ -225,7 +233,7 @@ pub(super) fn spawn_v5_loop(
|
||||
&id,
|
||||
LogLevel::Info,
|
||||
"connack",
|
||||
format!("连接成功 (session_present: {})", ack.session_present),
|
||||
i18n::tf("engine.connect_ok", &[("sp", &ack.session_present)]),
|
||||
None,
|
||||
);
|
||||
engine.emit(EngineEvent::Status {
|
||||
@@ -240,7 +248,10 @@ pub(super) fn spawn_v5_loop(
|
||||
&id,
|
||||
LogLevel::Error,
|
||||
"connack",
|
||||
format!("连接被拒绝: {:?} (code {code})", ack.code),
|
||||
i18n::tf(
|
||||
"engine.connack_rejected_code",
|
||||
&[("code", &format!("{:?}", ack.code)), ("n", &code)],
|
||||
),
|
||||
None,
|
||||
);
|
||||
engine.emit(EngineEvent::Status {
|
||||
@@ -259,7 +270,13 @@ pub(super) fn spawn_v5_loop(
|
||||
&id,
|
||||
LogLevel::Warn,
|
||||
"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,
|
||||
);
|
||||
engine.emit(EngineEvent::Status {
|
||||
@@ -278,26 +295,26 @@ pub(super) fn spawn_v5_loop(
|
||||
.map(|c| format!("{c:?}"))
|
||||
.collect();
|
||||
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(_))) => {
|
||||
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))) => {
|
||||
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))) => {
|
||||
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))) => {
|
||||
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))) => {
|
||||
engine.log(&id, LogLevel::Debug, "publish_outgoing",
|
||||
format!("消息已发出 (packet {pkid})"), None);
|
||||
i18n::tf("engine.publish_outgoing", &[("n", &pkid)]), None);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(V5Error::RequestsDone) => {
|
||||
@@ -322,19 +339,20 @@ pub(super) fn spawn_v5_loop(
|
||||
});
|
||||
if !auto_reconnect || first_connect {
|
||||
engine.log(&id, LogLevel::Error, "connection_error",
|
||||
format!("连接错误: {e}"), None);
|
||||
i18n::tf("engine.connection_error", &[("e", &e)]), None);
|
||||
break;
|
||||
}
|
||||
// max_reconnect_times>0 时按次数停止重试;0 表示无限重连
|
||||
if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times {
|
||||
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;
|
||||
}
|
||||
reconnect_attempts += 1;
|
||||
let delay = reconnect_delay(reconnect_attempts);
|
||||
let delay_s = format!("{:.1}", delay.as_secs_f32());
|
||||
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() 的取消信号要等一整个
|
||||
// 退避周期才可见,期间会对用户已关闭的连接继续重连。
|
||||
tokio::select! {
|
||||
@@ -345,7 +363,7 @@ pub(super) fn spawn_v5_loop(
|
||||
}
|
||||
}
|
||||
_ = 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user