mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
feat(profile): raise auto-update intervals below 24h
Each auto-update reloads the core, which drops every connection running through a `proxy-providers` entry. Existing profiles are corrected once at startup; the form warns about shorter intervals but still saves them.
This commit is contained in:
@@ -356,6 +356,27 @@ impl IProfiles {
|
||||
self.save_file().await
|
||||
}
|
||||
|
||||
/// Raise intervals below `min_minutes`. `None`/`0` mean "never auto-update", left alone.
|
||||
/// Returns how many changed so the caller can skip a pointless write.
|
||||
pub fn raise_short_update_intervals(&mut self, min_minutes: u64) -> usize {
|
||||
let mut raised = 0;
|
||||
|
||||
for item in self.items.iter_mut().flatten() {
|
||||
let Some(option) = item.option.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if option
|
||||
.update_interval
|
||||
.is_some_and(|interval| (1..min_minutes).contains(&interval))
|
||||
{
|
||||
option.update_interval = Some(min_minutes);
|
||||
raised += 1;
|
||||
}
|
||||
}
|
||||
|
||||
raised
|
||||
}
|
||||
|
||||
pub(crate) fn plan_delete_item(&mut self, uid: &String) -> Result<(bool, ProfileDeletePlan)> {
|
||||
let current = self.current.as_ref().unwrap_or(uid).clone();
|
||||
let delete_uids = self.get_item(uid)?.option.as_ref().map_or_else(Vec::new, |op| {
|
||||
@@ -1295,6 +1316,58 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn interval_item(uid: &str, update_interval: Option<u64>) -> PrfItem {
|
||||
PrfItem {
|
||||
uid: Some(uid.into()),
|
||||
itype: Some("remote".into()),
|
||||
option: Some(PrfOption {
|
||||
update_interval,
|
||||
..PrfOption::default()
|
||||
}),
|
||||
..PrfItem::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raising_intervals_only_touches_those_scheduled_too_often() {
|
||||
let mut profiles = IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![
|
||||
interval_item("too-often", Some(60)),
|
||||
interval_item("just-under", Some(1439)),
|
||||
interval_item("at-floor", Some(1440)),
|
||||
interval_item("relaxed", Some(4320)),
|
||||
// raising these would switch auto-update on
|
||||
interval_item("disabled-by-zero", Some(0)),
|
||||
interval_item("disabled-by-absence", None),
|
||||
PrfItem {
|
||||
uid: Some("no-option".into()),
|
||||
option: None,
|
||||
..PrfItem::default()
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
assert_eq!(profiles.raise_short_update_intervals(1440), 2);
|
||||
|
||||
let intervals: Vec<Option<u64>> = profiles
|
||||
.items
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|item| item.option.as_ref().and_then(|o| o.update_interval))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
intervals,
|
||||
vec![Some(1440), Some(1440), Some(1440), Some(4320), Some(0), None, None]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
profiles.raise_short_update_intervals(1440),
|
||||
0,
|
||||
"a second pass must be a no-op, so re-running the migration cannot churn the file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_plan_defers_files_and_selects_replacement() -> Result<()> {
|
||||
let mut profiles = IProfiles {
|
||||
|
||||
@@ -43,6 +43,11 @@ pub mod timing {
|
||||
pub const SERVICE_START_RETRY_DELAY: Duration = Duration::from_millis(300);
|
||||
}
|
||||
|
||||
pub mod profile {
|
||||
/// Floor, in minutes, for how often a profile may auto-update (24 hours).
|
||||
pub const MIN_UPDATE_INTERVAL: u64 = 1440;
|
||||
}
|
||||
|
||||
pub mod files {
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
@@ -21,6 +21,8 @@ pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||
pub static CLASH_CONFIG: &str = "config.yaml";
|
||||
pub static VERGE_CONFIG: &str = "verge.yaml";
|
||||
pub static PROFILE_YAML: &str = "profiles.yaml";
|
||||
/// Marks that the one-shot raise of too-short auto-update intervals has already run.
|
||||
pub static UPDATE_INTERVAL_MIGRATED: &str = ".update-interval-migrated";
|
||||
|
||||
/// init portable flag
|
||||
pub fn init_portable_flag() -> Result<()> {
|
||||
@@ -170,6 +172,10 @@ pub fn profiles_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(PROFILE_YAML))
|
||||
}
|
||||
|
||||
pub fn update_interval_migrated_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(UPDATE_INTERVAL_MIGRATED))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
let res_dir = app_resources_dir()?;
|
||||
|
||||
@@ -232,6 +232,52 @@ async fn migrate_legacy_macos_service_logs(log_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Raise existing sub-floor update intervals, once per install.
|
||||
///
|
||||
/// One-off repair, not a standing rule — the UI warns about shorter intervals but still saves
|
||||
/// them, so the marker is written even when nothing needed raising.
|
||||
pub async fn migrate_short_update_intervals() -> Result<()> {
|
||||
let marker = dirs::update_interval_migrated_path()?;
|
||||
if fs::try_exists(&marker).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Same order as every other writer holding it (lock, then draft permit): save_file runs
|
||||
// inside the closure, ahead of the optimistic check, so a racing restore can't be undone.
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let min = constants::profile::MIN_UPDATE_INTERVAL;
|
||||
let raised = Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles: IProfiles| async move {
|
||||
// `IProfiles::new` normalises items to Some when it read the file, and returns a
|
||||
// bare default() when it failed. A failed load also raises zero — without this the
|
||||
// marker would burn the one shot on profiles nobody managed to load.
|
||||
if profiles.items.is_none() {
|
||||
anyhow::bail!("profiles.yaml was not loaded; refusing to record the migration as done");
|
||||
}
|
||||
let raised = profiles.raise_short_update_intervals(min);
|
||||
if raised > 0 {
|
||||
profiles.save_file().await?;
|
||||
}
|
||||
Ok((profiles, raised))
|
||||
})
|
||||
.await?;
|
||||
|
||||
if raised > 0 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
"已将 {} 个订阅的自动更新间隔提升到 {} 分钟",
|
||||
raised,
|
||||
min
|
||||
);
|
||||
}
|
||||
|
||||
fs::write(&marker, b"").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn migrate_legacy_macos_logs() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
|
||||
@@ -48,6 +48,10 @@ pub fn resolve_setup_async() {
|
||||
AsyncHandler::spawn(|| async {
|
||||
logging!(info, Type::ClashVergeRev, "Version: {}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Ahead of the window and init_timer: anything saved or deep-link-imported after the app
|
||||
// is up is a deliberate choice. Config files already exist (init_work_dir_and_logger).
|
||||
logging_error!(Type::Setup, init::migrate_short_update_intervals().await);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
resolve_dock_show().await;
|
||||
init_startup_script().await;
|
||||
|
||||
@@ -35,6 +35,9 @@ export interface ProfileViewerRef {
|
||||
// remote / local
|
||||
type ProfileViewerProps = Props & { ref?: Ref<ProfileViewerRef> }
|
||||
|
||||
// 同后端 constants::profile::MIN_UPDATE_INTERVAL
|
||||
const MIN_UPDATE_INTERVAL = 1440
|
||||
|
||||
export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -354,23 +357,41 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<Controller
|
||||
name="option.update_interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
label={t('profiles.modals.profileForm.fields.updateInterval')}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t('shared.units.minutes')}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const interval = Number(field.value)
|
||||
// 0 和空值表示不自动更新,不提醒
|
||||
const tooFrequent =
|
||||
Number.isFinite(interval) &&
|
||||
interval > 0 &&
|
||||
interval < MIN_UPDATE_INTERVAL
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
label={t('profiles.modals.profileForm.fields.updateInterval')}
|
||||
helperText={
|
||||
tooFrequent
|
||||
? t(
|
||||
'profiles.modals.profileForm.warnings.frequentUpdate',
|
||||
{ minutes: MIN_UPDATE_INTERVAL },
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
slotProps={{
|
||||
formHelperText: { sx: { color: 'warning.main' } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t('shared.units.minutes')}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="option.with_proxy"
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "مُعرّف الملف الشخصي مفقود",
|
||||
"saveFailed": "تعذّر حفظ الملف الشخصي"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "الفاصل الزمني القصير يعيد تحميل النواة بشكل متكرر ويقطع الاتصالات النشطة. يُنصح بـ {{minutes}} دقيقة (24 ساعة)."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Profile creation failed, retrying with Clash proxy...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "Die Abonnement-Kennung fehlt",
|
||||
"saveFailed": "Abonnement konnte nicht gespeichert werden"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Ein kurzes Intervall lädt den Kern entsprechend häufig neu und trennt bestehende Verbindungen. Empfohlen werden {{minutes}} Minuten (24 Stunden)."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Erstellung des Abonnements fehlgeschlagen. Versuche es mit dem Clash-Proxy erneut...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "The profile identifier is missing",
|
||||
"saveFailed": "Failed to save profile"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "A short interval reloads the core that often and drops existing connections. {{minutes}} minutes (24 hours) is recommended."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Profile creation failed, retrying with Clash proxy...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "Falta el identificador de la suscripción",
|
||||
"saveFailed": "No se pudo guardar la suscripción"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Un intervalo corto recarga el núcleo con esa frecuencia y corta las conexiones existentes. Se recomiendan {{minutes}} minutos (24 horas)."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Error al crear la suscripción. Intentando con el proxy de Clash...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "شناسه پروفایل موجود نیست",
|
||||
"saveFailed": "ذخیره پروفایل ناموفق بود"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "بازهٔ کوتاه باعث بارگذاری مکرر هسته و قطع اتصالهای فعال میشود. {{minutes}} دقیقه (۲۴ ساعت) توصیه میشود."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Profile creation failed, retrying with Clash proxy...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "Pengenal profil tidak ada",
|
||||
"saveFailed": "Gagal menyimpan profil"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Interval yang singkat membuat inti dimuat ulang sesering itu dan memutus koneksi yang aktif. Disarankan {{minutes}} menit (24 jam)."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Profile creation failed, retrying with Clash proxy...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "プロファイル識別子がありません",
|
||||
"saveFailed": "プロファイルの保存に失敗しました"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "間隔が短いとその頻度でコアが再読み込みされ、既存の接続が切断されます。{{minutes}} 分(24 時間)を推奨します。"
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "プロファイルの作成に失敗しました。Clashプロキシを使用して再試行します...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "프로필 식별자가 없습니다",
|
||||
"saveFailed": "프로필을 저장하지 못했습니다"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "간격이 짧으면 코어가 그만큼 자주 다시 로드되어 기존 연결이 끊깁니다. {{minutes}}분(24시간)을 권장합니다."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "프로필 생성 실패, Clash 프록시로 다시 시도 중...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "Отсутствует идентификатор профиля",
|
||||
"saveFailed": "Не удалось сохранить профиль"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Короткий интервал часто перезагружает ядро и обрывает активные соединения. Рекомендуется {{minutes}} минут (24 часа)."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Не удалось создать профиль, повторная попытка через Clash proxy...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "Profil tanımlayıcısı eksik",
|
||||
"saveFailed": "Profil kaydedilemedi"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Kısa bir aralık çekirdeği o sıklıkta yeniden yükler ve mevcut bağlantıları koparır. {{minutes}} dakika (24 saat) önerilir."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Profil oluşturma başarısız oldu, Clash vekil ile yeniden deneniyor...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "Профиль билгесе юк",
|
||||
"saveFailed": "Профильне саклап булмады"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Кыска аралык ядрәне еш яңадан йөкли һәм актив тоташуларны өзә. {{minutes}} минут (24 сәгать) тәкъдим ителә."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Profile creation failed, retrying with Clash proxy...",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "缺少订阅标识",
|
||||
"saveFailed": "保存订阅失败"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "间隔过短会频繁触发内核重载并中断现有连接,建议使用默认值 {{minutes}} 分钟(24 小时)"
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "订阅创建失败,尝试使用 Clash 代理创建",
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"uidMissing": "缺少訂閱識別碼",
|
||||
"saveFailed": "儲存訂閱失敗"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "間隔過短會頻繁觸發核心重新載入並中斷現有連線,建議使用預設值 {{minutes}} 分鐘(24 小時)"
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "訂閱建立失敗,嘗試使用 Clash 代理建立",
|
||||
|
||||
@@ -233,6 +233,7 @@ export const translationKeys = [
|
||||
'profiles.modals.profileForm.errors.urlRequired',
|
||||
'profiles.modals.profileForm.errors.uidMissing',
|
||||
'profiles.modals.profileForm.errors.saveFailed',
|
||||
'profiles.modals.profileForm.warnings.frequentUpdate',
|
||||
'profiles.modals.profileForm.feedback.notifications.creationRetry',
|
||||
'profiles.modals.profileForm.feedback.notifications.creationSuccess',
|
||||
'profiles.modals.proxiesEditor.title',
|
||||
|
||||
@@ -393,6 +393,9 @@ export interface TranslationResources {
|
||||
local: string
|
||||
remote: string
|
||||
}
|
||||
warnings: {
|
||||
frequentUpdate: string
|
||||
}
|
||||
}
|
||||
proxiesEditor: {
|
||||
actions: {
|
||||
|
||||
Reference in New Issue
Block a user