//! 设置页「下载」分组 + 下载调优的**宿主侧翻译**(M193)。 //! //! ## 为什么单开一个文件 //! //! 同 `host/settings_tests.rs` 的理由:`host/settings.rs` 正好顶在 400 行硬线上,而本轮要往这一组加 //! 「`download.gh_proxy` 三态行 + 并发硬上限说明 + 调优翻译」约 24 行。不拆就得砍 `settings.rs` 里 //! 那些**留着防复发**的论证注释(`ai_group` 的 M174 回归说明、`parse_value` 的 P1-1 浮点键事故) //! ——那比多一个文件糟得多。搬出来还有一个附带好处:这一组是**唯一**要往 core 灌下载调优的一组, //! 代码与它的守卫(`settings_tests.rs`)现在挨在一起。 //! //! ## 谁在读 / 谁在写 //! //! ```text //! config/launcher.json ←── store.set(`settings::set`,本模块只读) //! download.concurrency ─┐ //! download.retry_limit ─┼─→ DownloadTuning(core 决定默认值与夹取)─→ shared::set_tuning //! download.gh_proxy ───┘ (引擎重建时生效) //! ``` //! //! **调优的同步点是显式的**:`host/engine.rs` 每次取引擎前、`dispatch.rs` 的 `SetSetting` 分支里, //! 都调 [`apply_tuning`](幂等:值没变就什么都不做)。它**不是**藏在某个读函数里的副作用—— //! 「读起来是纯读、实际改全局状态」正是本仓吃过亏的那类形状。 use unlucky_core::download::config::{ DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY, MAX_CONCURRENCY, }; use unlucky_core::download::shared; use crate::dto::{SettingGroupView, SettingRowView}; use super::super::env::HostEnv; use super::{int, selected, text}; /// 并发数的推荐档位(`selected()` 找不到时回 0;手改出来的非档位值会被插进选项表,见 [`group`])。 const CONCURRENCY_LADDER: [&str; 5] = ["4", "8", "16", "32", "64"]; /// 单源尝试次数的推荐档位(`0` 是旧版 GUI 提供过的档位,语义 = 只试一次,见 schema 描述)。 const RETRY_LADDER: [&str; 6] = ["0", "1", "2", "3", "5", "10"]; /// 下载(D7):并发/重试/地区/镜像偏好/代理/GitHub 加速前缀。 pub(super) fn group(env: &HostEnv) -> SettingGroupView { let concurrency = int( env, unlucky_core::download::config::KEY_CONCURRENCY, DEFAULT_MAX_CONCURRENCY as i64, ) .to_string(); let retry = int( env, unlucky_core::download::config::KEY_RETRY_LIMIT, i64::from(DEFAULT_MAX_ATTEMPTS_PER_SOURCE), ) .to_string(); let concurrency_options = ladder(&CONCURRENCY_LADDER, &concurrency); let retry_options = ladder(&RETRY_LADDER, &retry); let concurrency_options: Vec<&str> = concurrency_options.iter().map(String::as_str).collect(); let retry_options: Vec<&str> = retry_options.iter().map(String::as_str).collect(); let regions = ["auto", "cn", "global"]; let mirrors = ["auto", "official", "mirror"]; let proxies = ["none", "system", "manual"]; let region_value = text(env, "download.region"); let mirror_value = text(env, "download.mirror_preference"); let proxy_value = text(env, "download.proxy_mode"); SettingGroupView::new("download", "下载与镜像(D7)") .with(SettingRowView::choice( unlucky_core::download::config::KEY_CONCURRENCY, "并发数", &format!( "同时下载的文件数:出厂 {DEFAULT_MAX_CONCURRENCY},可调 1~{MAX_CONCURRENCY}(引擎硬上限,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效" ), &concurrency_options, selected(&concurrency_options, &concurrency), )) .with(SettingRowView::choice( unlucky_core::download::config::KEY_RETRY_LIMIT, "单源尝试次数", &format!( "同一个源最多试几次(含首次;出厂 {DEFAULT_MAX_ATTEMPTS_PER_SOURCE})。0 与 1 都等于「只试一次、不重试」——0 是旧版提供的档位,不代表「不下载」。换源不受此限" ), &retry_options, selected(&retry_options, &retry), )) .with(SettingRowView::choice( "download.region", "地区判定", "auto = 时区+语言+探测;决定是否优先国内镜像", ®ions, selected( ®ions, if region_value.is_empty() { "auto" } else { ®ion_value }, ), )) .with(SettingRowView::choice( "download.mirror_preference", "镜像偏好", "auto = 按健康度评分;official = 官方源优先", &mirrors, selected( &mirrors, if mirror_value.is_empty() { "auto" } else { &mirror_value }, ), )) .with(SettingRowView::choice( "download.proxy_mode", "代理模式", "none / system(跟随系统)/ manual(下面填地址)", &proxies, selected( &proxies, if proxy_value.is_empty() { "none" } else { &proxy_value }, ), )) .with(SettingRowView::text( "download.proxy_url", "HTTP 代理", "manual 模式生效;留空则用系统代理", &text(env, "download.proxy_url"), )) // M193:gh-proxy 三态。**留空与 off 是两件事**,描述里必须都写出来——用户最容易把 // 「我还没决定」当成「我不要加速」,于是以为自己关掉了、实际还在套前缀。 .with(SettingRowView::text( unlucky_core::download::config::KEY_GH_PROXY, "GitHub 加速前缀(gh-proxy)", "留空 = 跟随内置默认(镜像优先/国内判定时自动套 gh-proxy.com);填 off = 关闭(不生成任何 gh-proxy 候选);填一个 URL 前缀 = 覆盖它", &text(env, unlucky_core::download::config::KEY_GH_PROXY), )) .with(SettingRowView::action( super::super::engine::DETECT_REGION_ACTION, "重新检测地区", "清掉 24h 地区缓存,下次下载重新判定", "detect_region", )) } /// 推荐档位 + 「当前值不在档位里就把它插进去」(否则这一行会把 2 显示成 4 —— 又一个假值)。 fn ladder(ladder: &[&str], current: &str) -> Vec { let mut options: Vec = ladder.iter().map(|item| (*item).to_string()).collect(); if !current.is_empty() && !options.iter().any(|option| option == current) { options.push(current.to_string()); options.sort_by_key(|option| option.parse::().unwrap_or(i64::MAX)); } options } /// 从 `launcher.json` 读出下载调优(默认值与夹取口径在 core,本函数只做「取原始值」)。 #[must_use] pub(crate) fn tuning_of(env: &HostEnv) -> DownloadTuning { use unlucky_core::download::config::{KEY_CONCURRENCY, KEY_GH_PROXY, KEY_RETRY_LIMIT}; let store = env.store(); let doc = store.doc(); let gh = doc .effective_str(KEY_GH_PROXY) .map(str::to_string) .unwrap_or_default(); DownloadTuning::from_settings( doc.effective_i64(KEY_CONCURRENCY), doc.effective_i64(KEY_RETRY_LIMIT), Some(gh.as_str()), ) } /// 把当前设置灌进 core 的下载调优(按数据根),返回**是否真的变了**(变了就失效该根的引擎)。 /// /// 幂等:值没变时 `shared::set_tuning` 一行不动,所以「每次取引擎前都调」是安全的。 pub(crate) fn apply_tuning(env: &HostEnv) -> bool { shared::set_tuning(env.layout.cache(), tuning_of(env)) }