From c264224e7c46f515cfce4d0ff0b23d73642918f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Thu, 14 May 2026 06:54:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(rust):=20=E4=BF=AE=20CI=20=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=9A=84=204=20=E4=B8=AA=20clippy=20+=20fmt=20?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前 3 个 commit (cc19a07/1873e23/d97e196/6a0d874/bf55ca0) 推上去后 CI 全部 fail(三平台都 fail),原因不是 JS 改动而是历史 Rust 代码累积的 clippy/fmt 检查问题,本次一次性修齐让 CI 重新跑通。 ## 4 个 clippy errors(-D warnings 模式) 1. `field 'name' is never read` (HermesAttachment::name) - 该字段是前端可选传入的附件原文件名,目前后端未消费 - 加 #[allow(dead_code)] + 说明性 doc,保留字段供后续展开附件清单 UI 2. `unnecessary closure used to substitute value for Result::Err` × 2 - hermes_dashboard_api_proxy 里 `unwrap_or_else(|_| Value::String(body))` → `unwrap_or(Value::String(body))` - 闭包捕获 _ 但不用,直接用 unwrap_or 3. `manually reimplementing div_ceil` - base64_encode 里 `(bytes.len() + 2) / 3` → `bytes.len().div_ceil(3)`(标准库 1.73+ 支持) ## fmt 修复 hermes.rs 多处长 match arm + long argument list 不符合 rustfmt 默认风格, 跑 cargo fmt --all 自动修齐。 ## 验证 ✓ cargo fmt --all -- --check PASS ✓ cargo check PASS ✓ cargo clippy --all-targets -- -D warnings PASS(无 warning 无 error) ## 影响 - 不改变任何运行时行为 - 不影响前端 / 不影响 i18n / 不影响 Tauri 命令签名 - 纯代码风格 + lint 修复 --- src-tauri/src/commands/hermes.rs | 191 ++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 69 deletions(-) diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 9322b0e..e1523bc 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2109,8 +2109,8 @@ pub async fn hermes_read_config_full() -> Result { .map_err(|e| format!("Failed to read config.yaml: {e}"))?; // 解析 YAML → JSON - let yaml_value: serde_yaml::Value = serde_yaml::from_str(&raw) - .map_err(|e| format!("Invalid YAML in config.yaml: {e}"))?; + let yaml_value: serde_yaml::Value = + serde_yaml::from_str(&raw).map_err(|e| format!("Invalid YAML in config.yaml: {e}"))?; let config_json: Value = serde_json::to_value(&yaml_value) .map_err(|e| format!("YAML→JSON conversion failed: {e}"))?; @@ -2134,10 +2134,7 @@ pub async fn hermes_read_config_full() -> Result { let highlights: serde_json::Map = highlight_keys .iter() .map(|k| { - let v = config_json - .get(*k) - .cloned() - .unwrap_or(Value::Null); + let v = config_json.get(*k).cloned().unwrap_or(Value::Null); ((*k).to_string(), v) }) .collect(); @@ -2223,8 +2220,7 @@ async fn run_venv_python_json(script: &str) -> Result { .find(|l| !l.trim().is_empty()) .unwrap_or("") .trim(); - serde_json::from_str(last_line) - .map_err(|e| format!("Python 输出解析失败: {e}\n原文: {stdout}")) + serde_json::from_str(last_line).map_err(|e| format!("Python 输出解析失败: {e}\n原文: {stdout}")) } #[tauri::command] @@ -2247,8 +2243,8 @@ except Exception as e: pub async fn hermes_lazy_deps_status(features: Vec) -> Result { // 把 features 列表序列化成 Python 合法的列表字面量 // serde_json 的输出(如 ["platform.telegram","platform.discord"])正好是 Python 合法字面量 - let features_literal = serde_json::to_string(&features) - .map_err(|e| format!("features 序列化失败: {e}"))?; + let features_literal = + serde_json::to_string(&features).map_err(|e| format!("features 序列化失败: {e}"))?; let script = format!( r#" import json @@ -2273,8 +2269,8 @@ except Exception as e: #[tauri::command] pub async fn hermes_lazy_deps_ensure(feature: String) -> Result { // serde_json::to_string 把字符串包成 Python 合法的字符串字面量(已含引号 + escape) - let feature_literal = serde_json::to_string(&feature) - .map_err(|e| format!("feature 名序列化失败: {e}"))?; + let feature_literal = + serde_json::to_string(&feature).map_err(|e| format!("feature 名序列化失败: {e}"))?; let script = format!( r#" import json, sys @@ -3412,9 +3408,17 @@ fn normalize_hermes_stream_event( .map(|s| Value::String(s.to_string())) .unwrap_or(Value::Null); match event_type { - "message.delta" | "run.completed" | "run.failed" | "run.cancelled" - | "tool.started" | "tool.completed" | "tool.progress" | "tool.error" - | "reasoning.available" | "approval.request" | "approval.responded" => { + "message.delta" + | "run.completed" + | "run.failed" + | "run.cancelled" + | "tool.started" + | "tool.completed" + | "tool.progress" + | "tool.error" + | "reasoning.available" + | "approval.request" + | "approval.responded" => { let mut out = evt.clone(); if out.get("run_id").is_none() { out["run_id"] = Value::String(run_id.to_string()); @@ -3747,7 +3751,10 @@ pub async fn hermes_run_stop(run_id: String) -> Result { let body = resp.text().await.unwrap_or_default(); return Err(format!("stop 失败 HTTP {}: {}", status.as_u16(), body)); } - Ok(resp.json::().await.unwrap_or(serde_json::json!({ "ok": true }))) + Ok(resp + .json::() + .await + .unwrap_or(serde_json::json!({ "ok": true }))) } // --------------------------------------------------------------------------- @@ -3770,7 +3777,11 @@ pub async fn hermes_run_approval(run_id: String, choice: String) -> Result choice, - other => return Err(format!("approval choice 必须是 once/session/always/deny,收到 {other}")), + other => { + return Err(format!( + "approval choice 必须是 once/session/always/deny,收到 {other}" + )) + } }; let gw_url = hermes_gateway_url(); let url = format!("{gw_url}/v1/runs/{run_id}/approval"); @@ -3792,7 +3803,10 @@ pub async fn hermes_run_approval(run_id: String, choice: String) -> Result().await.unwrap_or(serde_json::json!({ "ok": true }))) + Ok(resp + .json::() + .await + .unwrap_or(serde_json::json!({ "ok": true }))) } // --------------------------------------------------------------------------- @@ -3830,7 +3844,9 @@ pub async fn hermes_run_status(run_id: String) -> Result { let body = resp.text().await.unwrap_or_default(); return Err(format!("status 失败 HTTP {}: {}", status.as_u16(), body)); } - resp.json::().await.map_err(|e| format!("解析 JSON 失败: {e}")) + resp.json::() + .await + .map_err(|e| format!("解析 JSON 失败: {e}")) } // --------------------------------------------------------------------------- @@ -3856,18 +3872,21 @@ pub async fn hermes_session_export(session_id: String) -> Result .build() .map_err(|e| format!("HTTP 客户端创建失败: {e}"))?; - let resp = client - .get(&url) - .send() - .await - .map_err(|e| format!("export 请求失败: {}(提示:请先启动 Dashboard)", reqwest_error_detail(&e)))?; + let resp = client.get(&url).send().await.map_err(|e| { + format!( + "export 请求失败: {}(提示:请先启动 Dashboard)", + reqwest_error_detail(&e) + ) + })?; let status = resp.status(); if !status.is_success() { let body = resp.text().await.unwrap_or_default(); return Err(format!("export 失败 HTTP {}: {}", status.as_u16(), body)); } // 让前端拿原始 JSON 自己打包下载(保留完整结构) - resp.json::().await.map_err(|e| format!("解析 JSON 失败: {e}")) + resp.json::() + .await + .map_err(|e| format!("解析 JSON 失败: {e}")) } // --------------------------------------------------------------------------- @@ -3990,10 +4009,12 @@ pub async fn hermes_dashboard_api_proxy( // 拿缓存的 token(首次为空,让 send 触发 401 再抓) let mut token = dashboard_session_token(port, false).await.ok(); - let resp = build_request(token.as_deref())? - .send() - .await - .map_err(|e| format!("Dashboard 请求失败: {}(提示:请先启动 Dashboard)", reqwest_error_detail(&e)))?; + let resp = build_request(token.as_deref())?.send().await.map_err(|e| { + format!( + "Dashboard 请求失败: {}(提示:请先启动 Dashboard)", + reqwest_error_detail(&e) + ) + })?; let status = resp.status(); if status.as_u16() == 401 { @@ -4008,15 +4029,14 @@ pub async fn hermes_dashboard_api_proxy( if !retry_status.is_success() { return Err(format!("HTTP {}: {}", retry_status.as_u16(), body)); } - return Ok(serde_json::from_str::(&body).unwrap_or_else(|_| Value::String(body))); + return Ok(serde_json::from_str::(&body).unwrap_or(Value::String(body))); } let resp_body = resp.text().await.unwrap_or_default(); if !status.is_success() { return Err(format!("HTTP {}: {}", status.as_u16(), resp_body)); } - Ok(serde_json::from_str::(&resp_body) - .unwrap_or_else(|_| Value::String(resp_body))) + Ok(serde_json::from_str::(&resp_body).unwrap_or(Value::String(resp_body))) } /// Batch 3 §K: 多模态附件结构 @@ -4027,7 +4047,9 @@ pub async fn hermes_dashboard_api_proxy( pub struct HermesAttachment { pub kind: String, pub mime: String, + /// 原始文件名(前端可选传入,用于日志/调试展示)— 当前未读取,保留供后续展开附件清单 UI 使用 #[serde(default)] + #[allow(dead_code)] pub name: Option, /// base64 编码的内容(不含 data:image/...,base64, 前缀,仅纯 base64) pub data_base64: String, @@ -6116,7 +6138,9 @@ fn multi_gw_pids_get(name: &str) -> Option { fn multi_gw_pids_set(name: &str, pid: u32) { if let Ok(mut guard) = MULTI_GW_PIDS.lock() { - guard.get_or_insert_with(HashMap::new).insert(name.to_string(), pid); + guard + .get_or_insert_with(HashMap::new) + .insert(name.to_string(), pid); } } @@ -6234,8 +6258,16 @@ pub async fn hermes_multi_gateway_list() -> Result { let configs = read_multi_gateways_config(); let mut result = Vec::new(); for cfg in configs { - let name = cfg.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(); - let profile = cfg.get("profile").and_then(|v| v.as_str()).unwrap_or("default").to_string(); + let name = cfg + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let profile = cfg + .get("profile") + .and_then(|v| v.as_str()) + .unwrap_or("default") + .to_string(); if name.is_empty() { continue; } @@ -6245,8 +6277,13 @@ pub async fn hermes_multi_gateway_list() -> Result { let pid_alive = pid_opt.map(pid_is_alive).unwrap_or(false); // TCP probe(即使 PID 死了,也可能其他进程占着端口) let addr = format!("127.0.0.1:{port}"); - let tcp_running = addr.parse::().ok() - .and_then(|sa| std::net::TcpStream::connect_timeout(&sa, std::time::Duration::from_millis(300)).ok()) + let tcp_running = addr + .parse::() + .ok() + .and_then(|sa| { + std::net::TcpStream::connect_timeout(&sa, std::time::Duration::from_millis(300)) + .ok() + }) .is_some(); result.push(serde_json::json!({ "name": name, @@ -6261,10 +6298,7 @@ pub async fn hermes_multi_gateway_list() -> Result { } #[tauri::command] -pub async fn hermes_multi_gateway_add( - name: String, - profile: String, -) -> Result { +pub async fn hermes_multi_gateway_add(name: String, profile: String) -> Result { let name = name.trim().to_string(); let profile = profile.trim().to_string(); if name.is_empty() { @@ -6274,11 +6308,17 @@ pub async fn hermes_multi_gateway_add( return Err("Profile 不能为空".into()); } // 名称合法性检查(同 hermes profile 规则) - if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { + if !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { return Err("名称只能含字母/数字/下划线/连字符".into()); } let mut configs = read_multi_gateways_config(); - if configs.iter().any(|c| c.get("name").and_then(|v| v.as_str()) == Some(&name)) { + if configs + .iter() + .any(|c| c.get("name").and_then(|v| v.as_str()) == Some(&name)) + { return Err(format!("名称 \"{name}\" 已存在")); } configs.push(serde_json::json!({ "name": name, "profile": profile })); @@ -6330,7 +6370,8 @@ pub async fn hermes_multi_gateway_start( } let addr = format!("127.0.0.1:{port}"); if let Ok(sa) = addr.parse::() { - if std::net::TcpStream::connect_timeout(&sa, std::time::Duration::from_millis(300)).is_ok() { + if std::net::TcpStream::connect_timeout(&sa, std::time::Duration::from_millis(300)).is_ok() + { return Err(format!( "端口 {port} 已被占用(非 ClawPanel spawn 的进程,无法接管。请用 services 页停掉默认 Gateway 后重试)" )); @@ -6340,8 +6381,8 @@ pub async fn hermes_multi_gateway_start( let enhanced = hermes_enhanced_path(); let home = hermes_home(); let log_path = home.join(format!("gateway-{name}-run.log")); - let log_file = std::fs::File::create(&log_path) - .map_err(|e| format!("创建日志文件失败: {e}"))?; + let log_file = + std::fs::File::create(&log_path).map_err(|e| format!("创建日志文件失败: {e}"))?; let log_err = log_file .try_clone() .map_err(|e| format!("克隆日志句柄失败: {e}"))?; @@ -6379,13 +6420,18 @@ pub async fn hermes_multi_gateway_start( std::mem::forget(child); // 不等待进程,由 PID 跟踪 multi_gw_pids_set(&name, pid); - let _ = app.emit("hermes-multi-gateway-changed", serde_json::json!({ "name": &name, "action": "started" })); + let _ = app.emit( + "hermes-multi-gateway-changed", + serde_json::json!({ "name": &name, "action": "started" }), + ); // 等端口起来(最多 8 秒) for _ in 0..40 { tokio::time::sleep(std::time::Duration::from_millis(200)).await; if let Ok(sa) = addr.parse::() { - if std::net::TcpStream::connect_timeout(&sa, std::time::Duration::from_millis(200)).is_ok() { + if std::net::TcpStream::connect_timeout(&sa, std::time::Duration::from_millis(200)) + .is_ok() + { return Ok(serde_json::json!({ "started": true, "pid": pid, "port": port })); @@ -6398,9 +6444,7 @@ pub async fn hermes_multi_gateway_start( } #[tauri::command] -pub async fn hermes_multi_gateway_stop( - name: String, -) -> Result { +pub async fn hermes_multi_gateway_stop(name: String) -> Result { let name = name.trim().to_string(); let pid = multi_gw_pids_get(&name); if pid.is_none() || !pid_is_alive(pid.unwrap()) { @@ -6438,8 +6482,8 @@ pub async fn hermes_multi_gateway_stop( // 提供:list / read / write 三个基础命令,前端组合成文件管理器 UI。 // ============================================================================ -const FS_MAX_READ_BYTES: u64 = 5 * 1024 * 1024; // 5 MB -const FS_MAX_LIST_ENTRIES: usize = 2000; // 单次最多返回 2000 条 +const FS_MAX_READ_BYTES: u64 = 5 * 1024 * 1024; // 5 MB +const FS_MAX_LIST_ENTRIES: usize = 2000; // 单次最多返回 2000 条 /// 验证路径在 hermes_home 子树内(防 path traversal)。 /// 返回安全的绝对路径,或 Err。 @@ -6456,10 +6500,7 @@ fn validate_hermes_fs_path(rel_path: &str) -> Result { let canonical_root = root.canonicalize().unwrap_or(root.clone()); let canonical_target = p.canonicalize().unwrap_or_else(|_| p.to_path_buf()); if !canonical_target.starts_with(&canonical_root) { - return Err(format!( - "路径必须在 {} 子树内", - root.to_string_lossy() - )); + return Err(format!("路径必须在 {} 子树内", root.to_string_lossy())); } canonical_target } else { @@ -6469,10 +6510,7 @@ fn validate_hermes_fs_path(rel_path: &str) -> Result { let canon = joined.canonicalize().unwrap_or(joined.clone()); let canonical_root = root.canonicalize().unwrap_or(root.clone()); if !canon.starts_with(&canonical_root) { - return Err(format!( - "路径不能跳出 {} 目录", - root.to_string_lossy() - )); + return Err(format!("路径不能跳出 {} 目录", root.to_string_lossy())); } canon } @@ -6494,16 +6532,20 @@ pub async fn hermes_fs_list(path: String) -> Result { for entry in read_dir.flatten().take(FS_MAX_LIST_ENTRIES) { let name = entry.file_name().to_string_lossy().to_string(); if name.starts_with('.') && name != ".env" && name != ".hermes" { - continue; // 隐藏文件默认不显示(.env 除外因为 Hermes 用它) + continue; // 隐藏文件默认不显示(.env 除外因为 Hermes 用它) } let ft = match entry.file_type() { Ok(t) => t, Err(_) => continue, }; let meta = entry.metadata().ok(); - let size = meta.as_ref().and_then(|m| if m.is_file() { Some(m.len()) } else { None }); + let size = meta + .as_ref() + .and_then(|m| if m.is_file() { Some(m.len()) } else { None }); let modified = meta.as_ref().and_then(|m| m.modified().ok()).and_then(|t| { - t.duration_since(std::time::UNIX_EPOCH).ok().map(|d| d.as_secs()) + t.duration_since(std::time::UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()) }); entries.push(serde_json::json!({ "name": name, @@ -6517,7 +6559,11 @@ pub async fn hermes_fs_list(path: String) -> Result { let ak = a.get("kind").and_then(|v| v.as_str()).unwrap_or(""); let bk = b.get("kind").and_then(|v| v.as_str()).unwrap_or(""); if ak != bk { - return if ak == "dir" { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater }; + return if ak == "dir" { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + }; } let an = a.get("name").and_then(|v| v.as_str()).unwrap_or(""); let bn = b.get("name").and_then(|v| v.as_str()).unwrap_or(""); @@ -6538,7 +6584,9 @@ pub async fn hermes_fs_read(path: String) -> Result { if !target.is_file() { return Err(format!("不是文件: {}", target.to_string_lossy())); } - let meta = target.metadata().map_err(|e| format!("读元数据失败: {e}"))?; + let meta = target + .metadata() + .map_err(|e| format!("读元数据失败: {e}"))?; if meta.len() > FS_MAX_READ_BYTES { return Err(format!( "文件过大({} bytes),最大 {} bytes", @@ -6566,10 +6614,11 @@ pub async fn hermes_fs_read(path: String) -> Result { /// 简单的 base64 编码(不引新依赖) fn base64_encode(bytes: &[u8]) -> String { const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4); + let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); let mut i = 0; while i + 3 <= bytes.len() { - let n = (u32::from(bytes[i]) << 16) | (u32::from(bytes[i + 1]) << 8) | u32::from(bytes[i + 2]); + let n = + (u32::from(bytes[i]) << 16) | (u32::from(bytes[i + 1]) << 8) | u32::from(bytes[i + 2]); out.push(CHARS[((n >> 18) & 0x3F) as usize] as char); out.push(CHARS[((n >> 12) & 0x3F) as usize] as char); out.push(CHARS[((n >> 6) & 0x3F) as usize] as char); @@ -6604,7 +6653,11 @@ pub async fn hermes_fs_write(path: String, content: String) -> Result FS_MAX_READ_BYTES { - return Err(format!("内容过大({} bytes),最大 {} bytes", content.len(), FS_MAX_READ_BYTES)); + return Err(format!( + "内容过大({} bytes),最大 {} bytes", + content.len(), + FS_MAX_READ_BYTES + )); } std::fs::write(&target, content.as_bytes()).map_err(|e| format!("写入失败: {e}"))?; let meta = target.metadata().ok();