mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-03 13:32:16 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b7e876ab0 | ||
|
|
bcbb5e3a58 | ||
|
|
726b2b362f | ||
|
|
a038e01cd5 | ||
|
|
f149604eb6 | ||
|
|
ebd78ae9ff | ||
|
|
3027e2b87f | ||
|
|
a0e57c5962 | ||
|
|
de15283ef6 | ||
|
|
3f39c2ceca |
Generated
+3
-3
@@ -1081,7 +1081,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.5.9"
|
version = "2.5.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1229,8 +1229,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clash_verge_service_ipc"
|
name = "clash_verge_service_ipc"
|
||||||
version = "2.7.6"
|
version = "2.7.7"
|
||||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.6#b2a7427d6681c8d3780ab83a73f4dc21da2f22d6"
|
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.7#3ed29700b093190b5e7addcaa892e77f172af544"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"kode-bridge",
|
"kode-bridge",
|
||||||
|
|||||||
+30
-20
@@ -1,19 +1,33 @@
|
|||||||
## v2.5.9
|
<!-- Release-notes skeleton. Entries accumulate in ./Changelog.md as PRs land;
|
||||||
|
before publishing a release, distill them:
|
||||||
- **Mihomo(Meta) 内核升级至 v1.19.33**
|
- user-visible changes only — no dev tooling, CI fixes, or refactors whose
|
||||||
|
effect is already covered by another entry
|
||||||
|
- describe outcomes, not internals: no internal module names, IPC or
|
||||||
|
protocol details, or implementation mechanics — write what the user
|
||||||
|
observes, not how the code achieves it
|
||||||
|
- one line per theme: merge entries that share a root cause
|
||||||
|
- each entry fits a single readable line (about 60 CJK characters); if it
|
||||||
|
overflows, it is packing multiple themes or detail that belongs in the
|
||||||
|
linked issue/PR — split it or drop the detail
|
||||||
|
- line grammar: open with a verb (修复/新增/优化), with the platform
|
||||||
|
first (Windows/macOS/Linux) when the change is platform-specific;
|
||||||
|
merge sub-symptoms as 主题:症状、症状; full-width punctuation,
|
||||||
|
「」 for UI labels, backticks for config keys and commands
|
||||||
|
- platform entries sit under their platform heading (bold, emoji
|
||||||
|
prefixed, ordered Windows/macOS/Linux, combined for pairs like
|
||||||
|
macOS/Linux) at the end of their section;
|
||||||
|
scripts-workflow/group_platforms.sh regroups, --check verifies
|
||||||
|
HTML comments are stripped by the update dialog's sanitizer and hidden on
|
||||||
|
GitHub, so they never reach users. -->
|
||||||
|
## v2.5.10
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口
|
||||||
- 修复 重启应用后定时自动备份不执行的问题
|
- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动的问题
|
||||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验的问题
|
||||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
- 修复订阅的 DNS 配置变化后,「DNS 覆写」实际已关闭但开关仍显示开启且没有提示的问题
|
||||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
|
||||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
|
||||||
- 修复 代理 在全局模式下无法展开节点组
|
|
||||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
|
||||||
- 修复 WebDAV 设置清空后无法保存
|
|
||||||
|
|
||||||
**🍎 macOS**
|
**🍎 macOS**
|
||||||
|
|
||||||
@@ -23,24 +37,20 @@
|
|||||||
|
|
||||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- Windows:修复安装程序重新安装或覆盖时未提示退出运行中应用,导致卸载报错或覆盖失败的问题
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
|
||||||
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
|
||||||
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
|
||||||
|
|
||||||
**🖥️ Windows**
|
|
||||||
|
|
||||||
- Windows 系统代理默认绕过 `*.local` 域名,访问局域网设备的 `.local` 主机名时不再经过代理
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -1,3 +1,51 @@
|
|||||||
|
## v2.5.9
|
||||||
|
|
||||||
|
- **Mihomo(Meta) 内核升级至 v1.19.33**
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
||||||
|
- 修复 重启应用后定时自动备份不执行的问题
|
||||||
|
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||||
|
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||||
|
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||||
|
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||||
|
- 修复 代理 在全局模式下无法展开节点组
|
||||||
|
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||||
|
- 修复 WebDAV 设置清空后无法保存
|
||||||
|
|
||||||
|
**🍎 macOS**
|
||||||
|
|
||||||
|
- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
|
||||||
|
|
||||||
|
**🐧 Linux**
|
||||||
|
|
||||||
|
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
|
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||||
|
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
||||||
|
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
|
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- Windows 系统代理默认绕过 `*.local` 域名,访问局域网设备的 `.local` 主机名时不再经过代理
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## v2.5.8
|
## v2.5.8
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.5.9",
|
"version": "2.5.10",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky || true",
|
"prepare": "husky || true",
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ fi
|
|||||||
|
|
||||||
tmpfile=$(mktemp)
|
tmpfile=$(mktemp)
|
||||||
{
|
{
|
||||||
cat "$CHANGELOG"
|
node -e '
|
||||||
|
const fs = require("node:fs");
|
||||||
|
process.stdout.write(fs.readFileSync(process.argv[1], "utf8").replace(/<!--[\s\S]*?-->/g, "").trimStart());
|
||||||
|
' "$CHANGELOG"
|
||||||
echo
|
echo
|
||||||
echo ""
|
echo ""
|
||||||
if [ -f "$HISTORY" ]; then
|
if [ -f "$HISTORY" ]; then
|
||||||
|
|||||||
@@ -652,9 +652,14 @@ function validateTag() {
|
|||||||
function prepareDraft() {
|
function prepareDraft() {
|
||||||
const tag = env.RELEASE_TAG
|
const tag = env.RELEASE_TAG
|
||||||
verifyTarget()
|
verifyTarget()
|
||||||
const release = list('releases?per_page=100').find(
|
const result = run('gh', [
|
||||||
(release) => release.tag_name === tag,
|
'api',
|
||||||
)
|
'--paginate',
|
||||||
|
endpoint('releases?per_page=100'),
|
||||||
|
'--jq',
|
||||||
|
`.[] | select(.tag_name == ${JSON.stringify(tag)}) | {id, draft}`,
|
||||||
|
])
|
||||||
|
const release = result ? JSON.parse(result) : null
|
||||||
if (release && !release.draft)
|
if (release && !release.draft)
|
||||||
throw new Error('This release is already public.')
|
throw new Error('This release is already public.')
|
||||||
const draft =
|
const draft =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.5.9"
|
version = "2.5.10"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -98,7 +98,7 @@ tracing = { workspace = true }
|
|||||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||||
async-trait = "0.1.92"
|
async-trait = "0.1.92"
|
||||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
|
||||||
"client",
|
"client",
|
||||||
] }
|
] }
|
||||||
arc-swap = "1.9.2"
|
arc-swap = "1.9.2"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ ManifestDPIAwareness PerMonitorV2
|
|||||||
!include "FileAssociation.nsh"
|
!include "FileAssociation.nsh"
|
||||||
!include "Win\COM.nsh"
|
!include "Win\COM.nsh"
|
||||||
!include "Win\Propkey.nsh"
|
!include "Win\Propkey.nsh"
|
||||||
|
!include "Win\RestartManager.nsh"
|
||||||
!include "WinVer.nsh"
|
!include "WinVer.nsh"
|
||||||
!include "LogicLib.nsh"
|
!include "LogicLib.nsh"
|
||||||
!include "StrFunc.nsh"
|
!include "StrFunc.nsh"
|
||||||
@@ -332,6 +333,8 @@ Function PageLeaveReinstall
|
|||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
reinst_uninstall:
|
reinst_uninstall:
|
||||||
|
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||||
|
|
||||||
HideWindow
|
HideWindow
|
||||||
ClearErrors
|
ClearErrors
|
||||||
|
|
||||||
@@ -368,7 +371,10 @@ Function PageLeaveReinstall
|
|||||||
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
||||||
Abort
|
Abort
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
Return
|
||||||
|
|
||||||
reinst_done:
|
reinst_done:
|
||||||
|
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
; 5. Choose install directory page
|
; 5. Choose install directory page
|
||||||
@@ -901,8 +907,7 @@ Section Install
|
|||||||
|
|
||||||
nsExec::Exec 'netsh int tcp res'
|
nsExec::Exec 'netsh int tcp res'
|
||||||
|
|
||||||
!include "Win\RestartManager.nsh"
|
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
|
||||||
!insertmacro CheckAllVergeProcesses
|
!insertmacro CheckAllVergeProcesses
|
||||||
|
|
||||||
; Ensure startup folders exist
|
; Ensure startup folders exist
|
||||||
@@ -1101,7 +1106,7 @@ Section Uninstall
|
|||||||
!insertmacro NSIS_HOOK_PREUNINSTALL
|
!insertmacro NSIS_HOOK_PREUNINSTALL
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||||
!insertmacro CheckAllVergeProcesses
|
!insertmacro CheckAllVergeProcesses
|
||||||
!insertmacro RemoveVergeService
|
!insertmacro RemoveVergeService
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
logging!(info, Type::Config, "Config regenerated successfully");
|
logging!(info, Type::Config, "Config regenerated successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,10 +191,6 @@ pub async fn delete_profile(index: std::string::String) -> CmdResult {
|
|||||||
drop(config_update_guard);
|
drop(config_update_guard);
|
||||||
drop(profile_write_guard);
|
drop(profile_write_guard);
|
||||||
|
|
||||||
if should_update {
|
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = Tray::global().update_tooltip().await {
|
if let Err(e) = Tray::global().update_tooltip().await {
|
||||||
logging!(warn, Type::Cmd, "异步更新托盘提示失败: {e:#}");
|
logging!(warn, Type::Cmd, "异步更新托盘提示失败: {e:#}");
|
||||||
}
|
}
|
||||||
@@ -264,7 +260,6 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult<ValidationO
|
|||||||
handle::Handle::notify_profile_changed(current);
|
handle::Handle::notify_profile_changed(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
Ok(ValidationOutcome::Valid)
|
Ok(ValidationOutcome::Valid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
utils::dirs,
|
utils::dirs,
|
||||||
};
|
};
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
@@ -168,7 +168,6 @@ async fn handle_saved_profile_file(
|
|||||||
);
|
);
|
||||||
match CoreManager::global().update_config_forced().await {
|
match CoreManager::global().update_config_forced().await {
|
||||||
Ok(outcome) if outcome.is_valid() => {
|
Ok(outcome) if outcome.is_valid() => {
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
Ok(ValidationOutcome::Valid)
|
Ok(ValidationOutcome::Valid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,6 +435,10 @@ impl CoreManager {
|
|||||||
{
|
{
|
||||||
anyhow::bail!("Sidecar did not become ready");
|
anyhow::bail!("Sidecar did not become ready");
|
||||||
}
|
}
|
||||||
|
// A Sidecar that was already running never loaded this generation.
|
||||||
|
if matches!(*mode, RunningMode::NotRunning) {
|
||||||
|
Config::runtime().await.apply();
|
||||||
|
}
|
||||||
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
||||||
self.apply_proxy_after_start().await
|
self.apply_proxy_after_start().await
|
||||||
}
|
}
|
||||||
@@ -492,6 +496,7 @@ impl CoreManager {
|
|||||||
{
|
{
|
||||||
anyhow::bail!("Sidecar did not become ready after service uninstall");
|
anyhow::bail!("Sidecar did not become ready after service uninstall");
|
||||||
}
|
}
|
||||||
|
Config::runtime().await.apply();
|
||||||
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
||||||
self.apply_proxy_after_start().await
|
self.apply_proxy_after_start().await
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ mod state;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||||
use clash_verge_logging::{LogRing, Type, logging};
|
use clash_verge_logging::{LogRing, Type, logging, logging_error};
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
sync::{
|
sync::{
|
||||||
@@ -274,6 +274,10 @@ impl CoreManager {
|
|||||||
|
|
||||||
pub(crate) fn finish_config_update(&self) {
|
pub(crate) fn finish_config_update(&self) {
|
||||||
self.config_update_in_progress.store(false, Ordering::Release);
|
self.config_update_in_progress.store(false, Ordering::Release);
|
||||||
|
// Spawned because the caller may still hold the config write lock this waits for.
|
||||||
|
crate::process::AsyncHandler::spawn(|| async {
|
||||||
|
logging_error!(Type::Config, crate::config::Config::sync_dns_override().await);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, level = "info", fields(retries = 0))]
|
#[tracing::instrument(skip_all, level = "info", fields(retries = 0))]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use clash_verge_service_ipc::{RemoteProvider, RuntimeAsset, RuntimeBundle};
|
|||||||
use serde_yaml_ng::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use sha2::{Digest as _, Sha256};
|
use sha2::{Digest as _, Sha256};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf, Prefix};
|
||||||
|
|
||||||
pub(crate) const GEO_ASSETS: &[&str] = &[
|
pub(crate) const GEO_ASSETS: &[&str] = &[
|
||||||
"Country.mmdb",
|
"Country.mmdb",
|
||||||
@@ -165,6 +165,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
|
|||||||
&mut assets,
|
&mut assets,
|
||||||
&mut remote_providers,
|
&mut remote_providers,
|
||||||
)?;
|
)?;
|
||||||
|
collect_ssh_private_keys(&mut config, &config_root, &mut destinations, &mut assets);
|
||||||
for filename in GEO_ASSETS {
|
for filename in GEO_ASSETS {
|
||||||
let source = config_root.join(filename);
|
let source = config_root.join(filename);
|
||||||
if source.is_file() && destinations.insert((*filename).to_string()) {
|
if source.is_file() && destinations.insert((*filename).to_string()) {
|
||||||
@@ -270,6 +271,75 @@ fn collect_provider_assets(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stages file-based ssh keys, which mihomo resolves against the core home directory.
|
||||||
|
fn collect_ssh_private_keys(
|
||||||
|
config: &mut Value,
|
||||||
|
config_root: &Path,
|
||||||
|
destinations: &mut HashSet<String>,
|
||||||
|
assets: &mut Vec<RuntimeAsset>,
|
||||||
|
) {
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 source = source.to_string_lossy().into_owned();
|
||||||
|
let destination = allocate_provider_destination("ssh-private-key", &source, destinations);
|
||||||
|
assets.push(RuntimeAsset {
|
||||||
|
source,
|
||||||
|
destination: 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<PathBuf> {
|
fn local_provider_source(config_root: &Path, raw_path: &str) -> Result<PathBuf> {
|
||||||
let path = Path::new(raw_path);
|
let path = Path::new(raw_path);
|
||||||
let source = if path.is_absolute() {
|
let source = if path.is_absolute() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::{
|
|||||||
CORE_REJECTED_PREFIX, OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch, PendingAction, RUN_STATE,
|
CORE_REJECTED_PREFIX, OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch, PendingAction, RUN_STATE,
|
||||||
ReadyWaitError, RunState, RunStateEnv, RunStateStore, ServiceHealth,
|
ReadyWaitError, RunState, RunStateEnv, RunStateStore, ServiceHealth,
|
||||||
},
|
},
|
||||||
runtime_bundle::{RemoteProviderRef, collect_runtime_bundle, remote_providers_of},
|
runtime_bundle::{GEO_ASSETS, RemoteProviderRef, collect_runtime_bundle, remote_providers_of},
|
||||||
tray::Tray,
|
tray::Tray,
|
||||||
},
|
},
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
@@ -101,6 +101,7 @@ struct ActiveServiceSession {
|
|||||||
proof: OwnerSessionProof,
|
proof: OwnerSessionProof,
|
||||||
supports_runtime_staging: bool,
|
supports_runtime_staging: bool,
|
||||||
supports_runtime_file_read: bool,
|
supports_runtime_file_read: bool,
|
||||||
|
supports_runtime_geo_read: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_service_session_token() -> Result<String> {
|
fn generate_service_session_token() -> Result<String> {
|
||||||
@@ -132,6 +133,13 @@ pub(crate) fn active_service_supports_runtime_file_read() -> bool {
|
|||||||
.is_some_and(|session| session.supports_runtime_file_read)
|
.is_some_and(|session| session.supports_runtime_file_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_service_supports_runtime_geo_read() -> bool {
|
||||||
|
ACTIVE_SERVICE_SESSION
|
||||||
|
.lock()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|session| session.supports_runtime_geo_read)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn clear_active_service_session() {
|
pub(crate) fn clear_active_service_session() {
|
||||||
ACTIVE_SERVICE_SESSION.lock().take();
|
ACTIVE_SERVICE_SESSION.lock().take();
|
||||||
}
|
}
|
||||||
@@ -140,6 +148,7 @@ pub(crate) fn clear_active_service_session() {
|
|||||||
struct ServiceCapabilities {
|
struct ServiceCapabilities {
|
||||||
runtime_staging: bool,
|
runtime_staging: bool,
|
||||||
runtime_file_read: bool,
|
runtime_file_read: bool,
|
||||||
|
runtime_geo_read: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceCapabilities {
|
impl ServiceCapabilities {
|
||||||
@@ -147,6 +156,7 @@ impl ServiceCapabilities {
|
|||||||
Self {
|
Self {
|
||||||
runtime_staging: info.supports_runtime_staging(),
|
runtime_staging: info.supports_runtime_staging(),
|
||||||
runtime_file_read: info.supports_runtime_file_read(),
|
runtime_file_read: info.supports_runtime_file_read(),
|
||||||
|
runtime_geo_read: info.supports_runtime_geo_read(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -956,6 +966,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
|||||||
},
|
},
|
||||||
supports_runtime_staging: capabilities.runtime_staging,
|
supports_runtime_staging: capabilities.runtime_staging,
|
||||||
supports_runtime_file_read: capabilities.runtime_file_read,
|
supports_runtime_file_read: capabilities.runtime_file_read,
|
||||||
|
supports_runtime_geo_read: capabilities.runtime_geo_read,
|
||||||
});
|
});
|
||||||
|
|
||||||
// PAC follows the Running Mode; the caller opens it via `core_started(Service)`.
|
// PAC follows the Running Mode; the caller opens it via `core_started(Service)`.
|
||||||
@@ -1206,6 +1217,62 @@ async fn sync_runtime_providers_by_service() -> Result<ProviderSync> {
|
|||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pulls geo databases the service core rewrote, because validation reads the app dir.
|
||||||
|
pub(crate) async fn sync_runtime_geo_by_service() {
|
||||||
|
if !active_service_supports_runtime_geo_read() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (Ok(credentials), Ok(session), Ok(app_dir)) = (
|
||||||
|
current_owner_credentials(),
|
||||||
|
active_service_session(),
|
||||||
|
dirs::app_home_dir(),
|
||||||
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for asset in GEO_ASSETS {
|
||||||
|
if let Err(error) = sync_runtime_geo(&credentials, &session, asset, &app_dir.join(asset)).await {
|
||||||
|
logging!(warn, Type::Service, "failed to sync geo database {asset}: {error:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_runtime_geo(
|
||||||
|
credentials: &OwnerCredentials,
|
||||||
|
session: &OwnerSessionProof,
|
||||||
|
asset: &str,
|
||||||
|
target: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Ok(local) = tokio::fs::metadata(target).await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some((Some(_), _, Some(runtime_mtime_ns))) = read_chunk(credentials, session, asset, u64::MAX).await? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
// A newer local file is a change the next staging still has to copy into the runtime.
|
||||||
|
if UNIX_EPOCH + Duration::from_nanos(runtime_mtime_ns) <= local.modified()? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let (temp, file) = create_sync_temp(target).await?;
|
||||||
|
let result = async {
|
||||||
|
if let Fetch::Complete {
|
||||||
|
mtime_ns: Some(mtime_ns),
|
||||||
|
..
|
||||||
|
} = fetch_runtime_file(credentials, session, asset, file).await?
|
||||||
|
{
|
||||||
|
// Matching the runtime copy's time lets the service skip copying these bytes straight back.
|
||||||
|
std::fs::File::options()
|
||||||
|
.write(true)
|
||||||
|
.open(&temp)?
|
||||||
|
.set_modified(UNIX_EPOCH + Duration::from_nanos(mtime_ns))?;
|
||||||
|
tokio::fs::rename(&temp, target).await?;
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
remove_temp(&temp).await;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
// The on-disk YAML may describe a rejected apply; use the committed runtime.
|
// The on-disk YAML may describe a rejected apply; use the committed runtime.
|
||||||
fn applied_remote_providers(runtime: &Draft<IRuntime>, app_dir: &Path) -> Result<Vec<RemoteProviderRef>> {
|
fn applied_remote_providers(runtime: &Draft<IRuntime>, app_dir: &Path) -> Result<Vec<RemoteProviderRef>> {
|
||||||
let applied = runtime.data_arc();
|
let applied = runtime.data_arc();
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ impl CoreConfigValidator {
|
|||||||
return Self::validate_file_syntax_outcome(config_path).await;
|
return Self::validate_file_syntax_outcome(config_path).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate::core::service::sync_runtime_geo_by_service().await;
|
||||||
let is_script = match Self::is_script_file(config_path).await {
|
let is_script = match Self::is_script_file(config_path).await {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -425,6 +426,8 @@ impl CoreConfigValidator {
|
|||||||
}
|
}
|
||||||
logging!(info, Type::Validate, "生成临时配置文件用于验证");
|
logging!(info, Type::Validate, "生成临时配置文件用于验证");
|
||||||
|
|
||||||
|
// The test run reads the app dir, which never sees updates made by a service core.
|
||||||
|
crate::core::service::sync_runtime_geo_by_service().await;
|
||||||
let fingerprint = validation_fingerprint(yaml).await;
|
let fingerprint = validation_fingerprint(yaml).await;
|
||||||
if *LAST_VALIDATED.lock() == Some(fingerprint) {
|
if *LAST_VALIDATED.lock() == Some(fingerprint) {
|
||||||
logging!(info, Type::Validate, "配置与上次通过验证的字节一致,跳过内核检查");
|
logging!(info, Type::Validate, "配置与上次通过验证的字节一致,跳过内核检查");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging};
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
@@ -88,7 +88,6 @@ async fn finalize_restored_verge_config(
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::{
|
|||||||
utils::help::{mask_err, mask_url},
|
utils::help::{mask_err, mask_url},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
|
|
||||||
/// Toggle proxy profile
|
/// Toggle proxy profile
|
||||||
@@ -217,10 +217,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
|
|||||||
if should_refresh {
|
if should_refresh {
|
||||||
logging!(debug, Type::Config, "[订阅更新] 更新内核配置");
|
logging!(debug, Type::Config, "[订阅更新] 更新内核配置");
|
||||||
match CoreManager::global().update_config_with_force(is_mannual_trigger).await {
|
match CoreManager::global().update_config_with_force(is_mannual_trigger).await {
|
||||||
Ok(outcome) if outcome.is_valid() => {
|
Ok(outcome) if outcome.is_valid() => announce(Refresh::Clash),
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
announce(Refresh::Clash);
|
|
||||||
}
|
|
||||||
Ok(outcome @ (ValidationOutcome::Skipped { .. } | ValidationOutcome::Busy)) if !is_mannual_trigger => {
|
Ok(outcome @ (ValidationOutcome::Skipped { .. } | ValidationOutcome::Busy)) if !is_mannual_trigger => {
|
||||||
logging!(info, Type::Config, "[订阅更新] 本次配置刷新已跳过: {}", outcome);
|
logging!(info, Type::Config, "[订阅更新] 本次配置刷新已跳过: {}", outcome);
|
||||||
}
|
}
|
||||||
@@ -247,9 +244,5 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
|
|||||||
|
|
||||||
/// 增强配置
|
/// 增强配置
|
||||||
pub async fn enhance_profiles() -> Result<ValidationOutcome> {
|
pub async fn enhance_profiles() -> Result<ValidationOutcome> {
|
||||||
let outcome = CoreManager::global().update_config_forced().await?;
|
CoreManager::global().update_config_forced().await
|
||||||
if outcome.is_valid() {
|
|
||||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
||||||
}
|
|
||||||
Ok(outcome)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ pub async fn hide() {
|
|||||||
{
|
{
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
}
|
}
|
||||||
|
handle::Handle::global().set_activation_policy_accessory();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -289,6 +289,9 @@ pub fn run() -> std::process::ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
handle::Handle::global().set_activation_policy_accessory();
|
||||||
|
|
||||||
if core::handle::Handle::global().is_exiting() {
|
if core::handle::Handle::global().is_exiting() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.5.9",
|
"version": "2.5.10",
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
|
|||||||
@@ -206,17 +206,20 @@ export const useRenderList = (
|
|||||||
verge?.default_latency_timeout,
|
verge?.default_latency_timeout,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const renderGroups = useMemo(() => {
|
||||||
|
if (!proxyView) return []
|
||||||
|
if (mode === 'rule') return proxyView.groups
|
||||||
|
const global = proxyView.global ? [proxyView.global] : []
|
||||||
|
return mode === 'global' ? [...global, ...proxyView.groups] : global
|
||||||
|
}, [mode, proxyView])
|
||||||
|
|
||||||
// Every group this list draws, so a test settling in any of them re-sorts that group.
|
// Every group this list draws, so a test settling in any of them re-sorts that group.
|
||||||
const renderedGroupNames = useMemo(() => {
|
const renderedGroupNames = useMemo(() => {
|
||||||
if (!proxyView) return []
|
if (!proxyView) return []
|
||||||
if (isChainMode)
|
if (isChainMode)
|
||||||
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
||||||
return mode === 'rule' || mode === 'global'
|
return renderGroups.map(({ name }) => name)
|
||||||
? proxyView.groups.map(({ name }) => name)
|
}, [isChainMode, proxyView, renderGroups, selectedGroup])
|
||||||
: proxyView.global
|
|
||||||
? [proxyView.global.name]
|
|
||||||
: []
|
|
||||||
}, [isChainMode, mode, proxyView, selectedGroup])
|
|
||||||
const groupDelays = useGroupsDelays(renderedGroupNames)
|
const groupDelays = useGroupsDelays(renderedGroupNames)
|
||||||
|
|
||||||
const groupCacheRef = useRef<Map<string, GroupCache>>(new Map())
|
const groupCacheRef = useRef<Map<string, GroupCache>>(new Map())
|
||||||
@@ -258,13 +261,7 @@ export const useRenderList = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useRule = mode === 'rule' || mode === 'global'
|
const useRule = mode === 'rule' || mode === 'global'
|
||||||
const renderGroups = useRule
|
|
||||||
? proxyView.groups
|
|
||||||
: proxyView.global === null
|
|
||||||
? []
|
|
||||||
: [proxyView.global]
|
|
||||||
const cache = groupCacheRef.current
|
const cache = groupCacheRef.current
|
||||||
let anyChanged = false
|
|
||||||
|
|
||||||
const retList = renderGroups.flatMap((group) => {
|
const retList = renderGroups.flatMap((group) => {
|
||||||
const headState = headStates[group.name] || DEFAULT_STATE
|
const headState = headStates[group.name] || DEFAULT_STATE
|
||||||
@@ -281,7 +278,6 @@ export const useRenderList = (
|
|||||||
return cached.items
|
return cached.items
|
||||||
}
|
}
|
||||||
|
|
||||||
anyChanged = true
|
|
||||||
const ret: IRenderItem[] = [
|
const ret: IRenderItem[] = [
|
||||||
{
|
{
|
||||||
type: 0,
|
type: 0,
|
||||||
@@ -350,7 +346,10 @@ export const useRenderList = (
|
|||||||
const filtered = !useRule
|
const filtered = !useRule
|
||||||
? retList.slice(1)
|
? retList.slice(1)
|
||||||
: retList.filter((item) => !item.group.hidden)
|
: retList.filter((item) => !item.group.hidden)
|
||||||
if (!anyChanged && prevListRef.current.length === filtered.length) {
|
if (
|
||||||
|
prevListRef.current.length === filtered.length &&
|
||||||
|
filtered.every((item, index) => item === prevListRef.current[index])
|
||||||
|
) {
|
||||||
return prevListRef.current
|
return prevListRef.current
|
||||||
}
|
}
|
||||||
prevListRef.current = filtered
|
prevListRef.current = filtered
|
||||||
@@ -364,6 +363,7 @@ export const useRenderList = (
|
|||||||
latencyTimeout,
|
latencyTimeout,
|
||||||
mode,
|
mode,
|
||||||
proxyView,
|
proxyView,
|
||||||
|
renderGroups,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user