diff --git a/Changelog.md b/Changelog.md
index 9c2f97ae1..69314efb6 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -25,6 +25,22 @@
🐞 修复问题
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口
+- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
+- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
+- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
+- 修复 首页 网站测速失败后卡片一直显示测试中
+- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
+- 修复 代理 在全局模式下无法展开节点组
+- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
+- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动的问题
+
+**🍎 macOS**
+
+- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
+
+**🐧 Linux**
+
+- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
**🖥️ Windows**
diff --git a/src-tauri/src/core/runtime_bundle.rs b/src-tauri/src/core/runtime_bundle.rs
index 9987d669c..67f405c3d 100644
--- a/src-tauri/src/core/runtime_bundle.rs
+++ b/src-tauri/src/core/runtime_bundle.rs
@@ -3,8 +3,8 @@ use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::{RemoteProvider, RuntimeAsset, RuntimeBundle};
use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
-use std::collections::{BTreeMap, HashSet};
-use std::path::{Component, Path, PathBuf};
+use std::collections::{BTreeMap, HashMap, HashSet};
+use std::path::{Component, Path, PathBuf, Prefix};
pub(crate) const GEO_ASSETS: &[&str] = &[
"Country.mmdb",
@@ -165,6 +165,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result,
+ assets: &mut Vec,
+) {
+ let Some(config) = config.as_mapping_mut() else {
+ return;
+ };
+ // mihomo parses `payload` for every provider type, eagerly as fallback proxies for file and http.
+ let mut proxy_lists = Vec::new();
+ for (key, value) in config.iter_mut() {
+ match key.as_str() {
+ Some("proxies") => proxy_lists.extend(value.as_sequence_mut()),
+ Some("proxy-providers") => proxy_lists.extend(
+ value
+ .as_mapping_mut()
+ .into_iter()
+ .flat_map(Mapping::values_mut)
+ .filter_map(|provider| provider.get_mut("payload").and_then(Value::as_sequence_mut)),
+ ),
+ _ => {}
+ }
+ }
+
+ let mut staged = HashMap::new();
+ for proxy in proxy_lists.into_iter().flatten().filter_map(Value::as_mapping_mut) {
+ if proxy.get("type").and_then(Value::as_str) != Some("ssh") {
+ continue;
+ }
+ // mihomo reads any value containing "PRIVATE KEY" as inline key material.
+ let Some(raw_path) = proxy
+ .get("private-key")
+ .and_then(Value::as_str)
+ .filter(|key| !key.is_empty() && !key.contains("PRIVATE KEY") && is_local_path(key))
+ else {
+ continue;
+ };
+ let Some(source) = local_provider_source(config_root, raw_path)
+ .ok()
+ .filter(|source| source.is_file())
+ else {
+ let name = proxy.get("name").and_then(Value::as_str).unwrap_or_default();
+ logging!(
+ warn,
+ Type::Config,
+ "ssh proxy {name:?} private key is unavailable below the config root; leaving it to the core"
+ );
+ continue;
+ };
+ // A root-level alias keeps the user's path away from service name rules and mihomo's inline-key test.
+ let destination = staged
+ .entry(source.to_string_lossy().into_owned())
+ .or_insert_with_key(|source| {
+ let destination = allocate_provider_destination("ssh-private-key", source, destinations);
+ assets.push(RuntimeAsset {
+ source: source.clone(),
+ destination: destination.clone(),
+ });
+ destination
+ })
+ .clone();
+ proxy.insert(Value::String("private-key".to_owned()), Value::String(destination));
+ }
+}
+
+/// Resolving a UNC or device path would authenticate to its server, so those stay with the core.
+fn is_local_path(raw_path: &str) -> bool {
+ match Path::new(raw_path).components().next() {
+ Some(Component::Prefix(prefix)) => matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)),
+ _ => true,
+ }
+}
+
fn local_provider_source(config_root: &Path, raw_path: &str) -> Result {
let path = Path::new(raw_path);
let source = if path.is_absolute() {