mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 03:03:46 +08:00
feat: auto-fallback to Clash proxy on scheduled subscription updates; refactor fallback logic and add request timeout
This commit is contained in:
parent
4d37e6870c
commit
8d62c0d521
@ -26,12 +26,15 @@
|
|||||||
- 移除 Webdav 跨平台备份恢复限制
|
- 移除 Webdav 跨平台备份恢复限制
|
||||||
- 增强 Webdav 备份目录检查和文件上传重试机制
|
- 增强 Webdav 备份目录检查和文件上传重试机制
|
||||||
- 系统代理守卫可检查意外设置变更并恢复
|
- 系统代理守卫可检查意外设置变更并恢复
|
||||||
|
- 定时自动订阅更新也能自动回退使用代理
|
||||||
|
- 订阅请求超时机制,防止订阅更新的时候卡死
|
||||||
|
|
||||||
#### 优化了:
|
#### 优化了:
|
||||||
- 系统代理 Bypass 设置
|
- 系统代理 Bypass 设置
|
||||||
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
|
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
|
||||||
- 增加 IP 信息请求重试机制,减少 Network Error 发生的情况
|
- 增加 IP 信息请求重试机制,减少 Network Error 发生的情况
|
||||||
- 切换到规则页面时自动刷新规则数据
|
- 切换到规则页面时自动刷新规则数据
|
||||||
|
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
|
||||||
|
|
||||||
## v2.2.3
|
## v2.2.3
|
||||||
|
|
||||||
|
@ -89,6 +89,12 @@ pub struct PrfOption {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub update_interval: Option<u64>,
|
pub update_interval: Option<u64>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// HTTP request timeout in seconds
|
||||||
|
/// default is 60 seconds
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timeout_seconds: Option<u64>,
|
||||||
|
|
||||||
/// for `remote` profile
|
/// for `remote` profile
|
||||||
/// disable certificate validation
|
/// disable certificate validation
|
||||||
/// default is `false`
|
/// default is `false`
|
||||||
@ -122,6 +128,7 @@ impl PrfOption {
|
|||||||
a.rules = b.rules.or(a.rules);
|
a.rules = b.rules.or(a.rules);
|
||||||
a.proxies = b.proxies.or(a.proxies);
|
a.proxies = b.proxies.or(a.proxies);
|
||||||
a.groups = b.groups.or(a.groups);
|
a.groups = b.groups.or(a.groups);
|
||||||
|
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||||
Some(a)
|
Some(a)
|
||||||
}
|
}
|
||||||
t => t.0.or(t.1),
|
t => t.0.or(t.1),
|
||||||
@ -240,12 +247,19 @@ impl PrfItem {
|
|||||||
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||||
|
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(60);
|
||||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||||
let mut script = opt_ref.and_then(|o| o.script.clone());
|
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||||
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||||
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
|
||||||
|
// 设置超时时间:连接超时10秒,请求总超时使用配置时间(默认60秒)
|
||||||
|
let mut builder = reqwest::ClientBuilder::new()
|
||||||
|
.use_rustls_tls()
|
||||||
|
.no_proxy()
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.timeout(std::time::Duration::from_secs(timeout));
|
||||||
|
|
||||||
// 使用软件自己的代理
|
// 使用软件自己的代理
|
||||||
if self_proxy {
|
if self_proxy {
|
||||||
|
@ -51,17 +51,73 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
let should_update = match url_opt {
|
let should_update = match url_opt {
|
||||||
Some((url, opt)) => {
|
Some((url, opt)) => {
|
||||||
println!("[订阅更新] 开始下载新的订阅内容");
|
println!("[订阅更新] 开始下载新的订阅内容");
|
||||||
let merged_opt = PrfOption::merge(opt, option);
|
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
|
||||||
|
|
||||||
println!("[订阅更新] 更新订阅配置");
|
// 尝试使用正常设置更新
|
||||||
let profiles = Config::profiles();
|
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||||
let mut profiles = profiles.latest();
|
Ok(item) => {
|
||||||
profiles.update_item(uid.clone(), item)?;
|
println!("[订阅更新] 更新订阅配置成功");
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let mut profiles = profiles.latest();
|
||||||
|
profiles.update_item(uid.clone(), item)?;
|
||||||
|
|
||||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||||
is_current
|
is_current
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// 首次更新失败,尝试使用Clash代理
|
||||||
|
println!("[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||||
|
|
||||||
|
// 发送通知
|
||||||
|
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||||
|
|
||||||
|
// 保存原始代理设置
|
||||||
|
let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy);
|
||||||
|
let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy);
|
||||||
|
|
||||||
|
// 创建使用Clash代理的选项
|
||||||
|
let mut fallback_opt = merged_opt.unwrap_or_default();
|
||||||
|
fallback_opt.with_proxy = Some(false);
|
||||||
|
fallback_opt.self_proxy = Some(true);
|
||||||
|
|
||||||
|
// 使用Clash代理重试
|
||||||
|
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||||
|
Ok(mut item) => {
|
||||||
|
println!("[订阅更新] 使用Clash代理更新成功");
|
||||||
|
|
||||||
|
// 恢复原始代理设置到item
|
||||||
|
if let Some(option) = item.option.as_mut() {
|
||||||
|
option.with_proxy = original_with_proxy;
|
||||||
|
option.self_proxy = original_self_proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新到配置
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let mut profiles = profiles.latest();
|
||||||
|
profiles.update_item(uid.clone(), item.clone())?;
|
||||||
|
|
||||||
|
// 获取配置名称用于通知
|
||||||
|
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
|
||||||
|
|
||||||
|
// 发送通知告知用户自动更新使用了回退机制
|
||||||
|
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||||
|
|
||||||
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
|
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||||
|
is_current
|
||||||
|
}
|
||||||
|
Err(retry_err) => {
|
||||||
|
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"update_failed_even_with_clash",
|
||||||
|
format!("{}", retry_err),
|
||||||
|
);
|
||||||
|
return Err(retry_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
@ -75,7 +131,7 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("[订阅更新] 更新失败: {}", err);
|
println!("[订阅更新] 更新失败: {}", err);
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,12 +183,6 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
||||||
|
|
||||||
// 存储原始设置以便回退后恢复
|
|
||||||
const originalOptions = {
|
|
||||||
with_proxy: itemData.option?.with_proxy,
|
|
||||||
self_proxy: itemData.option?.self_proxy
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据类型设置初始更新选项
|
// 根据类型设置初始更新选项
|
||||||
const option: Partial<IProfileOption> = {};
|
const option: Partial<IProfileOption> = {};
|
||||||
if (type === 0) {
|
if (type === 0) {
|
||||||
@ -205,31 +199,15 @@ export const ProfileItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试正常更新
|
// 调用后端更新(后端会自动处理回退逻辑)
|
||||||
await updateProfile(itemData.uid, option);
|
await updateProfile(itemData.uid, option);
|
||||||
|
|
||||||
|
// 更新成功,刷新列表
|
||||||
Notice.success(t("Update subscription successfully"));
|
Notice.success(t("Update subscription successfully"));
|
||||||
mutate("getProfiles");
|
mutate("getProfiles");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 更新失败,尝试使用自身代理
|
// 更新完全失败(包括后端的回退尝试)
|
||||||
const errmsg = err?.message || err.toString();
|
// 不需要做处理,后端会通过事件通知系统发送错误
|
||||||
Notice.info(t("Update failed, retrying with Clash proxy..."));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateProfile(itemData.uid, {
|
|
||||||
with_proxy: false,
|
|
||||||
self_proxy: true
|
|
||||||
});
|
|
||||||
|
|
||||||
Notice.success(t("Update with Clash proxy successfully"));
|
|
||||||
|
|
||||||
await updateProfile(itemData.uid, originalOptions);
|
|
||||||
mutate("getProfiles");
|
|
||||||
} catch (retryErr: any) {
|
|
||||||
const retryErrmsg = retryErr?.message || retryErr.toString();
|
|
||||||
Notice.error(
|
|
||||||
`${t("Update failed even with Clash proxy")}: ${retryErrmsg.replace(/error sending request for url (\S+?): /, "")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
||||||
}
|
}
|
||||||
|
@ -262,6 +262,25 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="option.timeout_seconds"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...text}
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
placeholder="60"
|
||||||
|
label={t("HTTP Request Timeout")}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{t("seconds")}</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -60,6 +60,18 @@ const handleNoticeMessage = (
|
|||||||
case "set_config::error":
|
case "set_config::error":
|
||||||
Notice.error(msg);
|
Notice.error(msg);
|
||||||
break;
|
break;
|
||||||
|
case "update_with_clash_proxy":
|
||||||
|
Notice.success(`${t("Update with Clash proxy successfully")} ${msg}`);
|
||||||
|
break;
|
||||||
|
case "update_retry_with_clash":
|
||||||
|
Notice.info(t("Update failed, retrying with Clash proxy..."));
|
||||||
|
break;
|
||||||
|
case "update_failed_even_with_clash":
|
||||||
|
Notice.error(`${t("Update failed even with Clash proxy")}: ${msg}`);
|
||||||
|
break;
|
||||||
|
case "update_failed":
|
||||||
|
Notice.error(msg);
|
||||||
|
break;
|
||||||
case "config_validate::boot_error":
|
case "config_validate::boot_error":
|
||||||
Notice.error(`${t("Boot Config Validation Failed")} ${msg}`);
|
Notice.error(`${t("Boot Config Validation Failed")} ${msg}`);
|
||||||
break;
|
break;
|
||||||
|
@ -263,6 +263,8 @@ const ProfilePage = () => {
|
|||||||
try {
|
try {
|
||||||
await updateProfile(uid);
|
await updateProfile(uid);
|
||||||
throttleMutate();
|
throttleMutate();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`更新订阅 ${uid} 失败:`, err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingCache((cache) => ({ ...cache, [uid]: false }));
|
setLoadingCache((cache) => ({ ...cache, [uid]: false }));
|
||||||
}
|
}
|
||||||
|
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@ -207,6 +207,7 @@ interface IProfileOption {
|
|||||||
with_proxy?: boolean;
|
with_proxy?: boolean;
|
||||||
self_proxy?: boolean;
|
self_proxy?: boolean;
|
||||||
update_interval?: number;
|
update_interval?: number;
|
||||||
|
timeout_seconds?: number;
|
||||||
danger_accept_invalid_certs?: boolean;
|
danger_accept_invalid_certs?: boolean;
|
||||||
merge?: string;
|
merge?: string;
|
||||||
script?: string;
|
script?: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user