mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
feat(enhance): warn when extensions write app-owned keys
This commit is contained in:
+1
-2
@@ -3,11 +3,9 @@
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 回退 2.5.4 引入的 DNS 覆写保护,修复重启后开关自动关闭
|
||||
- 优化文件错误日志,显示具体文件路径和失败原因
|
||||
- 修复 DNS 覆写:空内容报错、初始值覆盖已有设置、已填写字段残留原配置、空字段在高级编辑器中缺失
|
||||
- 修复首次以管理员身份启动后,系统服务因数据目录所有权不符而拒绝启动内核的问题
|
||||
- 修复 DNS 覆写:空内容报错、初始值覆盖已有设置、已填写字段残留原配置、空字段在高级编辑器中缺失
|
||||
|
||||
</details>
|
||||
|
||||
@@ -16,5 +14,6 @@
|
||||
|
||||
- 优化系统服务版本检查:版本与当前应用要求不一致时,提示卸载后重新安装
|
||||
- 优化预构建资源,补充 ASN 数据库文件
|
||||
- 扩展配置或脚本写入由应用设置接管的字段时,弹出提示并在扩展日志中记录该值已被丢弃
|
||||
|
||||
</details>
|
||||
|
||||
@@ -32,6 +32,11 @@ pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String
|
||||
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn take_discarded_keys_notice() -> Option<String> {
|
||||
crate::enhance::take_discarded_keys_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
|
||||
@@ -14,6 +14,7 @@ use self::{
|
||||
tun::use_tun,
|
||||
};
|
||||
use crate::config::dns::{DnsOverrideState, dns_override_source};
|
||||
use crate::core::handle::Handle;
|
||||
use crate::utils::dirs;
|
||||
use crate::{
|
||||
config::{Config, IProfiles, IVerge, PrfItem},
|
||||
@@ -22,6 +23,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -247,16 +249,33 @@ async fn process_global_items(
|
||||
global_merge: ChainItem,
|
||||
global_script: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
let notes: ResultLog = authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(global_merge.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
let (res_config, changed_keys, logs) = use_script(script, config, profile_name.clone()).await;
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
config = res_config;
|
||||
logs.extend(
|
||||
authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(global_script.uid, logs);
|
||||
}
|
||||
|
||||
@@ -329,6 +348,60 @@ impl AuthoritativeFields {
|
||||
let config = enforce_tun(config, self.tun);
|
||||
enforce_dns_ipv6(config, self.dns_ipv6)
|
||||
}
|
||||
|
||||
/// The owned values as `config` holds them now, for diffing one override.
|
||||
fn current(&self, config: &Mapping) -> Self {
|
||||
let tun_keys: Vec<Value> = self.tun.keys().cloned().collect();
|
||||
Self::capture(config, &tun_keys, self.dns_ipv6.is_some())
|
||||
}
|
||||
|
||||
/// Owned keys an override moved off the app value; `enforce` discards those writes.
|
||||
fn overridden(&self, before: &Self, after: &Self) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
for &key in CONTROL_PLANE_KEYS {
|
||||
if before.control_plane.get(key) != after.control_plane.get(key)
|
||||
&& after.control_plane.get(key) != self.control_plane.get(key)
|
||||
{
|
||||
keys.push(key.into());
|
||||
}
|
||||
}
|
||||
for key in self.tun.keys() {
|
||||
if before.tun.get(key) != after.tun.get(key) && after.tun.get(key) != self.tun.get(key) {
|
||||
keys.push(format!("tun.{}", key.as_str().unwrap_or_default()).into());
|
||||
}
|
||||
}
|
||||
if self.dns_ipv6.is_some() && before.dns_ipv6 != after.dns_ipv6 && after.dns_ipv6 != self.dns_ipv6 {
|
||||
keys.push("dns.ipv6".into());
|
||||
}
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
fn discarded_note(key: String) -> (String, String) {
|
||||
(
|
||||
"warn".into(),
|
||||
format!("`{key}` is managed by Settings; the value written here was discarded").into(),
|
||||
)
|
||||
}
|
||||
|
||||
static PENDING_DISCARDED_KEYS: Mutex<Option<String>> = Mutex::new(None);
|
||||
static LAST_DISCARDED_KEYS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
|
||||
pub(crate) fn take_discarded_keys_notice() -> Option<String> {
|
||||
PENDING_DISCARDED_KEYS.lock().take()
|
||||
}
|
||||
|
||||
/// One notice per distinct set, so regenerations with unchanged extensions stay quiet.
|
||||
fn notify_discarded_keys(keys: Vec<String>) {
|
||||
let mut last = LAST_DISCARDED_KEYS.lock();
|
||||
if *last == keys {
|
||||
return;
|
||||
}
|
||||
if !keys.is_empty() {
|
||||
*PENDING_DISCARDED_KEYS.lock() = Some(keys.join(", ").into());
|
||||
Handle::notice_message("enhance::discarded_keys", "");
|
||||
}
|
||||
*last = keys;
|
||||
}
|
||||
|
||||
/// 手动 merge/script 前保存 app 最终控制面值,只记录当前存在的键。
|
||||
@@ -459,16 +532,33 @@ async fn process_profile_items(
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
let notes: ResultLog = authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let (res_config, changed_keys, logs) = use_script(script, config, profile_name.clone()).await;
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
config = res_config;
|
||||
logs.extend(
|
||||
authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
@@ -836,12 +926,22 @@ pub async fn enhance(
|
||||
global_merge,
|
||||
global_script,
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (config, exists_keys, result_map) =
|
||||
process_profile_items(config, exists_keys, result_map, merge_item, script_item, &profile_name).await;
|
||||
let (config, exists_keys, result_map) = process_profile_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
merge_item,
|
||||
script_item,
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
|
||||
notify_discarded_keys(authoritative.overridden(&authoritative, &authoritative.current(&config)));
|
||||
let config = authoritative.enforce(config);
|
||||
let config = ensure_lan_bind_address(config);
|
||||
|
||||
@@ -1312,7 +1412,7 @@ mod authoritative_field_tests {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ChainItem, ChainType, cleanup_proxy_groups, ensure_lan_bind_address, process_global_items,
|
||||
AuthoritativeFields, ChainItem, ChainType, cleanup_proxy_groups, ensure_lan_bind_address, process_global_items,
|
||||
process_profile_items, use_keys,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
@@ -1365,6 +1465,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let profile_name = "test-profile".into();
|
||||
let authoritative = AuthoritativeFields::capture(&config, &[], false);
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
config,
|
||||
exists_keys,
|
||||
@@ -1372,6 +1473,7 @@ mod tests {
|
||||
global_merge,
|
||||
global_script,
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, exists_keys, _) = process_profile_items(
|
||||
@@ -1381,6 +1483,7 @@ mod tests {
|
||||
profile_merge,
|
||||
profile_script,
|
||||
&profile_name,
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ mod app_init {
|
||||
cmd::set_dns_override,
|
||||
cmd::take_dns_override_notice,
|
||||
cmd::take_service_fallback_notice,
|
||||
cmd::take_discarded_keys_notice,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
cmd::get_clash_logs,
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "كتبت الإضافات الحقول {{keys}} التي تديرها الإعدادات، لذلك تم تجاهل هذه القيم",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "Erweiterungen haben {{keys}} gesetzt, die von den Einstellungen verwaltet werden, daher wurden diese Werte verworfen",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "Extensions wrote {{keys}}, which Settings manages, so those values were discarded",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "Las extensiones escribieron {{keys}}, campos gestionados por los ajustes, por lo que esos valores se descartaron",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "افزونهها فیلدهای {{keys}} را نوشتند که توسط تنظیمات مدیریت میشوند، بنابراین این مقادیر نادیده گرفته شدند",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "Ekstensi menulis {{keys}} yang dikelola oleh Pengaturan, sehingga nilai tersebut diabaikan",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "拡張設定またはスクリプトが、アプリ設定で管理される {{keys}} を書き込んだため、その値は破棄されました",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "강제 새로고침 완료",
|
||||
"discardedKeys": "확장 설정 또는 스크립트가 앱 설정이 관리하는 {{keys}} 필드를 작성하여 해당 값은 무시되었습니다",
|
||||
"emergencyRefreshFailed": "긴급 새로고침 실패: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Принудительное обновление завершено",
|
||||
"discardedKeys": "Расширения записали поля {{keys}}, которыми управляют настройки приложения, поэтому эти значения отброшены",
|
||||
"emergencyRefreshFailed": "Экстренное обновление не удалось: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Zorunlu yenileme tamamlandı",
|
||||
"discardedKeys": "Uzantılar, Ayarlar tarafından yönetilen {{keys}} alanlarını yazdığı için bu değerler yok sayıldı",
|
||||
"emergencyRefreshFailed": "Acil yenileme başarısız oldu: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "Киңәйтмәләр Көйләүләр белән идарә ителә торган {{keys}} кырларын язды, шуңа күрә бу кыйммәтләр кире кагылды",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "数据已强制刷新",
|
||||
"discardedKeys": "扩展配置或脚本写入了由应用设置接管的字段 {{keys}},这些值已被丢弃",
|
||||
"emergencyRefreshFailed": "紧急刷新失败: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Force refresh completed",
|
||||
"discardedKeys": "擴充設定或腳本寫入了由應用程式設定接管的欄位 {{keys}},這些值已被捨棄",
|
||||
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
|
||||
@@ -14,6 +14,7 @@ vi.mock('@/hooks/use-profiles', () => ({ revalidateProfiles: vi.fn() }))
|
||||
vi.mock('@/hooks/use-system-state', () => ({ runStateQueryKey: ['state'] }))
|
||||
vi.mock('@/services/events', () => ({ subscribeVergeEvents: vi.fn() }))
|
||||
vi.mock('@/services/cmds', () => ({
|
||||
takeDiscardedKeysNotice: vi.fn().mockResolvedValue(null),
|
||||
takeDnsOverrideNotice: vi.fn(),
|
||||
takeServiceFallbackNotice: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
@@ -64,6 +64,7 @@ export const useLayoutEvents = (
|
||||
() => {
|
||||
revalidateKeys(['getRuntimeState', 'getVergeConfig'])
|
||||
handleNotice(['dns_override::auto_disabled', ''])
|
||||
handleNotice(['enhance::discarded_keys', ''])
|
||||
handleNotice(['service_core::sidecar_fallback', ''])
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
takeDiscardedKeysNotice,
|
||||
takeDnsOverrideNotice,
|
||||
takeServiceFallbackNotice,
|
||||
} from '@/services/cmds'
|
||||
@@ -55,6 +56,22 @@ export const handleNoticeMessage = (
|
||||
console.error('Failed to read the pending DNS override notice', error)
|
||||
})
|
||||
},
|
||||
'enhance::discarded_keys': () => {
|
||||
void takeDiscardedKeysNotice()
|
||||
.then((keys) => {
|
||||
if (keys) {
|
||||
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
|
||||
keys,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'Failed to read the pending discarded keys notice',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
'tun_mode::auto_disabled': () =>
|
||||
showNotice.info(
|
||||
'settings.sections.system.notifications.tunMode.autoDisabled',
|
||||
|
||||
@@ -186,6 +186,10 @@ export async function takeServiceFallbackNotice() {
|
||||
return invoke<boolean>('take_service_fallback_notice')
|
||||
}
|
||||
|
||||
export async function takeDiscardedKeysNotice() {
|
||||
return invoke<string | null>('take_discarded_keys_notice')
|
||||
}
|
||||
|
||||
export async function getSystemProxy() {
|
||||
return invoke<{
|
||||
enable: boolean
|
||||
|
||||
@@ -186,6 +186,7 @@ export const translationKeys = [
|
||||
'profiles.page.feedback.notifications.switchBusy',
|
||||
'profiles.page.feedback.notifications.batchDeleted',
|
||||
'profiles.page.feedback.notices.forceRefreshCompleted',
|
||||
'profiles.page.feedback.notices.discardedKeys',
|
||||
'profiles.page.feedback.notices.emergencyRefreshFailed',
|
||||
'profiles.page.feedback.tooltips.forceRefreshStaleData',
|
||||
'profiles.page.title',
|
||||
|
||||
@@ -446,6 +446,7 @@ export interface TranslationResources {
|
||||
updateFailed: string
|
||||
}
|
||||
notices: {
|
||||
discardedKeys: string
|
||||
emergencyRefreshFailed: string
|
||||
forceRefreshCompleted: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user